]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: save
authordaiwei <daiwei521@126.com>
Sat, 10 May 2025 14:18:11 +0000 (22:18 +0800)
committerdaiwei <daiwei521@126.com>
Sun, 11 May 2025 14:09:13 +0000 (22:09 +0800)
packages/runtime-core/src/renderer.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index 79965b28a9cdcc9815d70128812252c17fdfccba..d1278bda31f895140e8e229a3e8a9e026d42a72c 100644 (file)
@@ -711,6 +711,18 @@ function baseCreateRenderer(
     if (needCallTransitionHooks) {
       transition!.beforeEnter(el)
     }
+
+    // For custom element with shadowRoot: false, the anchor node may be moved
+    // to the slot container. In this case, it need to use the anchor's parent
+    // node as the actual container.
+    if (
+      container._isVueCE &&
+      container._def.shadowRoot === false &&
+      anchor &&
+      anchor.$parentNode
+    ) {
+      container = anchor.$parentNode
+    }
     hostInsert(el, container, anchor)
     if (
       (vnodeHook = props && props.onVnodeMounted) ||
@@ -966,7 +978,7 @@ function baseCreateRenderer(
           !isSameVNodeType(oldVNode, newVNode) ||
           // - In the case of a component, it could contain anything.
           oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
-          ? hostParentNode(oldVNode.el) || oldVNode.el.$parentNode
+          ? hostParentNode(oldVNode.el)!
           : // In other cases, the parent container is not actually used so we
             // just pass the block element here to avoid a DOM parentNode call.
             fallbackContainer
index 02522fbd92bc4c9818269ecb35f55e64781c6ddc..0fe0b2275c04a3d464d007b4b6bf02c5d3c5a208 100644 (file)
@@ -1212,7 +1212,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="">` +
-          `<div>false</div><!--v-if-->` +
+          `<div>false</div><!--v-if--><!--v-if-->` +
           `</ce-shadow-root-false-optimized>`,
       )
 
@@ -1228,7 +1228,7 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="">` +
-          `<div>false</div><!--v-if-->` +
+          `<div>false</div><!--v-if--><!--v-if-->` +
           `</ce-shadow-root-false-optimized>`,
       )
 
@@ -1236,12 +1236,12 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
-          `<div><div>true</div><div>hi</div></div>` +
+          `<!--v-if--><div><div>true</div><div>hi</div></div>` +
           `</ce-shadow-root-false-optimized>`,
       )
     })
 
-    test.todo('update slotted v-if nodes w/ shadowRoot false', async () => {
+    test('update slotted v-if nodes w/ shadowRoot false', async () => {
       const E = defineCustomElement(
         defineComponent({
           props: {
@@ -1298,25 +1298,33 @@ describe('defineCustomElement', () => {
       const app = createApp(App)
       app.mount(container)
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app=""><div>false</div><!--v-if--></ce-shadow-root-false>`,
+        `<ce-shadow-root-false data-v-app="">` +
+          `<div>false</div><!--v-if--><!--v-if-->` +
+          `</ce-shadow-root-false>`,
       )
 
       click()
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app="" is-shown=""><div><div>true</div><!--v-if--></div></ce-shadow-root-false>`,
+        `<ce-shadow-root-false data-v-app="" is-shown="">` +
+          `<div><div>true</div><!--v-if--></div>` +
+          `</ce-shadow-root-false>`,
       )
 
       click()
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app=""><div>false</div><!--v-if--></ce-shadow-root-false>`,
+        `<ce-shadow-root-false data-v-app="">` +
+          `<div>false</div><!--v-if--><!--v-if-->` +
+          `</ce-shadow-root-false>`,
       )
 
       click()
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app="" is-shown=""><div><div>true</div><div>hi</div></div></ce-shadow-root-false>`,
+        `<ce-shadow-root-false data-v-app="" is-shown="">` +
+          `<!--v-if--><div><div>true</div><div>hi</div></div>` +
+          `</ce-shadow-root-false>`,
       )
     })
 
@@ -1389,7 +1397,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
-          `fallback` +
+          `<!--v-if-->fallback` +
           `</ce-with-fallback-shadow-root-false-optimized>`,
       )
 
@@ -1405,90 +1413,87 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
-          `fallback<!--v-if-->` +
+          `<!--v-if-->fallback` +
           `</ce-with-fallback-shadow-root-false-optimized>`,
       )
     })
 
-    test.todo(
-      'switch between slotted and fallback nodes w/ shadowRoot false',
-      async () => {
-        const E = defineCustomElement(
-          defineComponent({
-            render() {
-              return renderSlot(this.$slots, 'foo', {}, () => [
-                createTextVNode('fallback'),
-              ])
-            },
-          }),
-          { shadowRoot: false },
-        )
-        customElements.define('ce-with-fallback-shadow-root-false', E)
-
-        const Comp = defineComponent({
+    test('switch between slotted and fallback nodes w/ shadowRoot false', async () => {
+      const E = defineCustomElement(
+        defineComponent({
           render() {
-            return h('ce-with-fallback-shadow-root-false', null, [
-              this.$slots.foo
-                ? h('div', { key: 0, slot: 'foo' }, [
-                    renderSlot(this.$slots, 'foo'),
-                  ])
-                : createCommentVNode('v-if', true),
-              renderSlot(this.$slots, 'default'),
+            return renderSlot(this.$slots, 'foo', {}, () => [
+              createTextVNode('fallback'),
             ])
           },
-        })
+        }),
+        { shadowRoot: false },
+      )
+      customElements.define('ce-with-fallback-shadow-root-false', E)
 
-        const isShown = ref(false)
-        const App = defineComponent({
-          components: { Comp },
-          render() {
-            return h(
-              Comp,
-              null,
-              createSlots(
-                { _: 2 /* DYNAMIC */ } as any,
-                [
-                  isShown.value
-                    ? {
-                        name: 'foo',
-                        fn: withCtx(() => [createTextVNode('foo')]),
-                        key: '0',
-                      }
-                    : undefined,
-                ] as any,
-              ),
-            )
-          },
-        })
+      const Comp = defineComponent({
+        render() {
+          return h('ce-with-fallback-shadow-root-false', null, [
+            this.$slots.foo
+              ? h('div', { key: 0, slot: 'foo' }, [
+                  renderSlot(this.$slots, 'foo'),
+                ])
+              : createCommentVNode('v-if', true),
+            renderSlot(this.$slots, 'default'),
+          ])
+        },
+      })
 
-        const container = document.createElement('div')
-        document.body.appendChild(container)
-
-        const app = createApp(App)
-        app.mount(container)
-        expect(container.innerHTML).toBe(
-          `<ce-with-fallback-shadow-root-false data-v-app="">` +
-            `fallback` +
-            `</ce-with-fallback-shadow-root-false>`,
-        )
-
-        isShown.value = true
-        await nextTick()
-        expect(container.innerHTML).toBe(
-          `<ce-with-fallback-shadow-root-false data-v-app="">` +
-            `<div slot="foo">foo</div>` +
-            `</ce-with-fallback-shadow-root-false>`,
-        )
-
-        isShown.value = false
-        await nextTick()
-        expect(container.innerHTML).toBe(
-          `<ce-with-fallback-shadow-root-false data-v-app="">` +
-            `fallback<!--v-if-->` +
-            `</ce-with-fallback-shadow-root-false>`,
-        )
-      },
-    )
+      const isShown = ref(false)
+      const App = defineComponent({
+        components: { Comp },
+        render() {
+          return h(
+            Comp,
+            null,
+            createSlots(
+              { _: 2 /* DYNAMIC */ } as any,
+              [
+                isShown.value
+                  ? {
+                      name: 'foo',
+                      fn: withCtx(() => [createTextVNode('foo')]),
+                      key: '0',
+                    }
+                  : undefined,
+              ] as any,
+            ),
+          )
+        },
+      })
+
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+
+      const app = createApp(App)
+      app.mount(container)
+      expect(container.innerHTML).toBe(
+        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+          `<!--v-if-->fallback` +
+          `</ce-with-fallback-shadow-root-false>`,
+      )
+
+      isShown.value = true
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+          `<div slot="foo">foo</div>` +
+          `</ce-with-fallback-shadow-root-false>`,
+      )
+
+      isShown.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+          `<!--v-if-->fallback` +
+          `</ce-with-fallback-shadow-root-false>`,
+      )
+    })
   })
 
   describe('helpers', () => {
index a6a4b1ba1e0df4a4dfbe1545f4fd31d1ec2bb9ee..0d9c1754e809cb6f8b5ab6c340733237dc176191 100644 (file)
@@ -247,6 +247,7 @@ export class VueElement
   private _slots?: Record<string, Node[]>
   private _slotFallbacks?: Record<string, Node[]>
   private _slotAnchors?: Map<string, Node>
+  private _slotNames: Set<string> | undefined
 
   constructor(
     /**
@@ -632,10 +633,8 @@ export class VueElement
       const slotName =
         (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
       ;(slots[slotName] || (slots[slotName] = [])).push(n)
+      ;(this._slotNames || (this._slotNames = new Set())).add(slotName)
       const next = n.nextSibling
-      // store the parentNode reference since node will be removed
-      // but it is needed during patching
-      ;(n as any).$parentNode = n.parentNode
       if (remove) this.removeChild(n)
       n = next
     }
@@ -647,7 +646,6 @@ export class VueElement
   private _renderSlots() {
     const outlets = (this._teleportTarget || this).querySelectorAll('slot')
     const scopeId = this._instance!.type.__scopeId
-    this._slotAnchors = new Map()
     const processedSlots = new Set<string>()
 
     for (let i = 0; i < outlets.length; i++) {
@@ -659,7 +657,10 @@ export class VueElement
 
       // insert an anchor to facilitate updates
       const anchor = document.createTextNode('')
-      this._slotAnchors.set(slotName, anchor)
+      ;(this._slotAnchors || (this._slotAnchors = new Map())).set(
+        slotName,
+        anchor,
+      )
       parent.insertBefore(anchor, o)
 
       if (content) {
@@ -679,11 +680,25 @@ export class VueElement
     if (!processedSlots.has('default')) {
       let content = this._slots!['default']
       if (content) {
-        // TODO
-        content = content.filter(
-          n => !(n.nodeType === 8 && (n as Comment).data === 'v-if'),
+        let anchor
+        // if the default slot is not the first one, insert it behind the previous slot
+        if (this._slotAnchors) {
+          const slotNames = Array.from(this._slotNames!)
+          const defaultSlotIndex = slotNames.indexOf('default')
+          if (defaultSlotIndex > 0) {
+            const prevSlotAnchor = this._slotAnchors.get(
+              slotNames[defaultSlotIndex - 1],
+            )
+            if (prevSlotAnchor) anchor = prevSlotAnchor.nextSibling
+          }
+        }
+
+        insertSlottedContent(
+          content,
+          scopeId,
+          this._root,
+          anchor || this.firstChild,
         )
-        insertSlottedContent(content, scopeId, this, this.firstChild)
       }
     }
   }
@@ -722,8 +737,8 @@ export class VueElement
         const fallbackNodes = this._slotFallbacks![name]
         if (fallbackNodes) {
           // render fallback nodes for removed slots
-          if (!newSlotNames.includes(name)) {
-            const anchor = this._slotAnchors!.get(name)!
+          if (!newSlotNames.includes(name) && this._slotAnchors) {
+            const anchor = this._slotAnchors.get(name)!
             fallbackNodes.forEach(fallbackNode =>
               this.insertBefore(fallbackNode, anchor),
             )
@@ -830,6 +845,7 @@ function insertSlottedContent(
         ;(child as Element).setAttribute(id, '')
       }
     }
+    ;(n as any).$parentNode = parent
     parent.insertBefore(n, anchor)
   }
 }