]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(custom-element): delay mounting of custom elements with async parent
authorEvan You <evan@vuejs.org>
Tue, 6 Aug 2024 18:18:54 +0000 (02:18 +0800)
committerEvan You <evan@vuejs.org>
Tue, 6 Aug 2024 18:18:54 +0000 (02:18 +0800)
close #8127
close #9341
close #9351

the fix is based on #9351 with reused tests

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

index 70af88ed466e4a73d97ae39b552ac62f64bd269c..52e677ea625dd2484a42127550ad37c2fae4f98c 100644 (file)
@@ -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')
+  })
 })
index 4c3be8d449480753f36ef9ae7755a1b31f770a41..2684e97ea51ae843d3e1ed4bc0cfcddff1b6b8f2 100644 (file)
@@ -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