Skip to content

Commit 37ccb9b

Browse files
committed
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
1 parent 03a9ea2 commit 37ccb9b

File tree

2 files changed

+125
-15
lines changed

2 files changed

+125
-15
lines changed

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

+85
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
h,
1111
inject,
1212
nextTick,
13+
provide,
1314
ref,
1415
render,
1516
renderSlot,
@@ -1032,4 +1033,88 @@ describe('defineCustomElement', () => {
10321033
).toHaveBeenWarned()
10331034
})
10341035
})
1036+
1037+
test('async & nested custom elements', async () => {
1038+
let fooVal: string | undefined = ''
1039+
const E = defineCustomElement(
1040+
defineAsyncComponent(() => {
1041+
return Promise.resolve({
1042+
setup(props) {
1043+
provide('foo', 'foo')
1044+
},
1045+
render(this: any) {
1046+
return h('div', null, [renderSlot(this.$slots, 'default')])
1047+
},
1048+
})
1049+
}),
1050+
)
1051+
1052+
const EChild = defineCustomElement({
1053+
setup(props) {
1054+
fooVal = inject('foo')
1055+
},
1056+
render(this: any) {
1057+
return h('div', null, 'child')
1058+
},
1059+
})
1060+
customElements.define('my-el-async-nested-ce', E)
1061+
customElements.define('slotted-child', EChild)
1062+
container.innerHTML = `<my-el-async-nested-ce><div><slotted-child></slotted-child></div></my-el-async-nested-ce>`
1063+
1064+
await new Promise(r => setTimeout(r))
1065+
const e = container.childNodes[0] as VueElement
1066+
expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
1067+
expect(fooVal).toBe('foo')
1068+
})
1069+
1070+
test('async & multiple levels of nested custom elements', async () => {
1071+
let fooVal: string | undefined = ''
1072+
let barVal: string | undefined = ''
1073+
const E = defineCustomElement(
1074+
defineAsyncComponent(() => {
1075+
return Promise.resolve({
1076+
setup(props) {
1077+
provide('foo', 'foo')
1078+
},
1079+
render(this: any) {
1080+
return h('div', null, [renderSlot(this.$slots, 'default')])
1081+
},
1082+
})
1083+
}),
1084+
)
1085+
1086+
const EChild = defineCustomElement({
1087+
setup(props) {
1088+
provide('bar', 'bar')
1089+
},
1090+
render(this: any) {
1091+
return h('div', null, [renderSlot(this.$slots, 'default')])
1092+
},
1093+
})
1094+
1095+
const EChild2 = defineCustomElement({
1096+
setup(props) {
1097+
fooVal = inject('foo')
1098+
barVal = inject('bar')
1099+
},
1100+
render(this: any) {
1101+
return h('div', null, 'child')
1102+
},
1103+
})
1104+
customElements.define('my-el-async-nested-m-ce', E)
1105+
customElements.define('slotted-child-m', EChild)
1106+
customElements.define('slotted-child2-m', EChild2)
1107+
container.innerHTML =
1108+
`<my-el-async-nested-m-ce>` +
1109+
`<div><slotted-child-m>` +
1110+
`<slotted-child2-m></slotted-child2-m>` +
1111+
`</slotted-child-m></div>` +
1112+
`</my-el-async-nested-m-ce>`
1113+
1114+
await new Promise(r => setTimeout(r))
1115+
const e = container.childNodes[0] as VueElement
1116+
expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
1117+
expect(fooVal).toBe('foo')
1118+
expect(barVal).toBe('bar')
1119+
})
10351120
})

packages/runtime-dom/src/apiCustomElement.ts

+40-15
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ export class VueElement
207207
private _resolved = false
208208
private _numberProps: Record<string, true> | null = null
209209
private _styleChildren = new WeakSet()
210+
private _pendingResolve: Promise<void> | undefined
211+
private _parent: VueElement | undefined
210212
/**
211213
* dev only
212214
*/
@@ -257,15 +259,42 @@ export class VueElement
257259
this._parseSlots()
258260
}
259261
this._connected = true
262+
263+
// locate nearest Vue custom element parent for provide/inject
264+
let parent: Node | null = this
265+
while (
266+
(parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
267+
) {
268+
if (parent instanceof VueElement) {
269+
this._parent = parent
270+
break
271+
}
272+
}
273+
260274
if (!this._instance) {
261275
if (this._resolved) {
276+
this._setParent()
262277
this._update()
263278
} else {
264-
this._resolveDef()
279+
if (parent && parent._pendingResolve) {
280+
this._pendingResolve = parent._pendingResolve.then(() => {
281+
this._pendingResolve = undefined
282+
this._resolveDef()
283+
})
284+
} else {
285+
this._resolveDef()
286+
}
265287
}
266288
}
267289
}
268290

291+
private _setParent(parent = this._parent) {
292+
if (parent) {
293+
this._instance!.parent = parent._instance
294+
this._instance!.provides = parent._instance!.provides
295+
}
296+
}
297+
269298
disconnectedCallback() {
270299
this._connected = false
271300
nextTick(() => {
@@ -285,7 +314,9 @@ export class VueElement
285314
* resolve inner component definition (handle possible async component)
286315
*/
287316
private _resolveDef() {
288-
this._resolved = true
317+
if (this._pendingResolve) {
318+
return
319+
}
289320

290321
// set initial attrs
291322
for (let i = 0; i < this.attributes.length; i++) {
@@ -302,6 +333,9 @@ export class VueElement
302333
this._ob.observe(this, { attributes: true })
303334

304335
const resolve = (def: InnerComponentDef, isAsync = false) => {
336+
this._resolved = true
337+
this._pendingResolve = undefined
338+
305339
const { props, styles } = def
306340

307341
// cast Number-type props set before resolve
@@ -346,7 +380,9 @@ export class VueElement
346380

347381
const asyncDef = (this._def as ComponentOptions).__asyncLoader
348382
if (asyncDef) {
349-
asyncDef().then(def => resolve((this._def = def), true))
383+
this._pendingResolve = asyncDef().then(def =>
384+
resolve((this._def = def), true),
385+
)
350386
} else {
351387
resolve(this._def)
352388
}
@@ -486,18 +522,7 @@ export class VueElement
486522
}
487523
}
488524

489-
// locate nearest Vue custom element parent for provide/inject
490-
let parent: Node | null = this
491-
while (
492-
(parent =
493-
parent && (parent.parentNode || (parent as ShadowRoot).host))
494-
) {
495-
if (parent instanceof VueElement) {
496-
instance.parent = parent._instance
497-
instance.provides = parent._instance!.provides
498-
break
499-
}
500-
}
525+
this._setParent()
501526
}
502527
}
503528
return vnode

0 commit comments

Comments
 (0)