]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: save
authordaiwei <daiwei521@126.com>
Fri, 9 May 2025 08:54:54 +0000 (16:54 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 9 May 2025 08:54:54 +0000 (16:54 +0800)
packages/runtime-core/src/renderer.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index d75efb890cd99fc6fefaad1649c5203e1b3d83ba..79965b28a9cdcc9815d70128812252c17fdfccba 100644 (file)
@@ -966,7 +966,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) || oldVNode.el.$parentNode
           : // 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 85f1d0820aa527c6ab7736864521ba6af1cc3f79..02522fbd92bc4c9818269ecb35f55e64781c6ddc 100644 (file)
@@ -1141,7 +1141,7 @@ describe('defineCustomElement', () => {
     })
 
     // #13206
-    test('update slotted v-if nodes w/ shadowRoot false', async () => {
+    test('update slotted v-if nodes w/ shadowRoot false (optimized mode)', async () => {
       const E = defineCustomElement(
         defineComponent({
           props: {
@@ -1155,16 +1155,18 @@ describe('defineCustomElement', () => {
         }),
         { shadowRoot: false },
       )
-      customElements.define('ce-shadow-root-false', E)
+      customElements.define('ce-shadow-root-false-optimized', E)
 
       const Comp = defineComponent({
         props: {
           isShown: { type: Boolean, required: true },
         },
         render() {
-          return h('ce-shadow-root-false', { 'is-shown': this.isShown }, [
-            renderSlot(this.$slots, 'default'),
-          ])
+          return h(
+            'ce-shadow-root-false-optimized',
+            { 'is-shown': this.isShown },
+            [renderSlot(this.$slots, 'default')],
+          )
         },
       })
 
@@ -1185,7 +1187,12 @@ describe('defineCustomElement', () => {
               { isShown: isShown.value },
               {
                 default: withCtx(() => [
-                  createElementVNode('div', null, isShown.value, 1 /* TEXT */),
+                  createElementVNode(
+                    'div',
+                    null,
+                    String(isShown.value),
+                    1 /* TEXT */,
+                  ),
                   count.value > 1
                     ? (openBlock(), createElementBlock('div', { key: 0 }, 'hi'))
                     : createCommentVNode('v-if', true),
@@ -1204,7 +1211,94 @@ describe('defineCustomElement', () => {
       const app = createApp(App)
       app.mount(container)
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app=""><!--v-if--></ce-shadow-root-false>`,
+        `<ce-shadow-root-false-optimized data-v-app="">` +
+          `<div>false</div><!--v-if-->` +
+          `</ce-shadow-root-false-optimized>`,
+      )
+
+      click()
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
+          `<div><div>true</div><!--v-if--></div>` +
+          `</ce-shadow-root-false-optimized>`,
+      )
+
+      click()
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<ce-shadow-root-false-optimized data-v-app="">` +
+          `<div>false</div><!--v-if-->` +
+          `</ce-shadow-root-false-optimized>`,
+      )
+
+      click()
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
+          `<div><div>true</div><div>hi</div></div>` +
+          `</ce-shadow-root-false-optimized>`,
+      )
+    })
+
+    test.todo('update slotted v-if nodes w/ shadowRoot false', async () => {
+      const E = defineCustomElement(
+        defineComponent({
+          props: {
+            isShown: { type: Boolean, required: true },
+          },
+          render() {
+            return this.isShown
+              ? h('div', { key: 0 }, [renderSlot(this.$slots, 'default')])
+              : createCommentVNode('v-if')
+          },
+        }),
+        { shadowRoot: false },
+      )
+      customElements.define('ce-shadow-root-false', E)
+
+      const Comp = defineComponent({
+        props: {
+          isShown: { type: Boolean, required: true },
+        },
+        render() {
+          return h('ce-shadow-root-false', { 'is-shown': this.isShown }, [
+            renderSlot(this.$slots, 'default'),
+          ])
+        },
+      })
+
+      const isShown = ref(false)
+      const count = ref(0)
+
+      function click() {
+        isShown.value = !isShown.value
+        count.value++
+      }
+
+      const App = {
+        render() {
+          return h(
+            Comp,
+            { isShown: isShown.value },
+            {
+              default: () => [
+                h('div', null, String(isShown.value)),
+                count.value > 1
+                  ? h('div', { key: 0 }, 'hi')
+                  : createCommentVNode('v-if', true),
+              ],
+            },
+          )
+        },
+      }
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+
+      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>`,
       )
 
       click()
@@ -1216,7 +1310,7 @@ describe('defineCustomElement', () => {
       click()
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-shadow-root-false data-v-app=""><!--v-if--></ce-shadow-root-false>`,
+        `<ce-shadow-root-false data-v-app=""><div>false</div><!--v-if--></ce-shadow-root-false>`,
       )
 
       click()
@@ -1227,7 +1321,7 @@ describe('defineCustomElement', () => {
     })
 
     // #13234
-    test('switch between slotted and fallback nodes w/ shadowRoot false', async () => {
+    test('switch between slotted and fallback nodes w/ shadowRoot false (optimized mode)', async () => {
       const E = defineCustomElement(
         defineComponent({
           render() {
@@ -1238,21 +1332,25 @@ describe('defineCustomElement', () => {
         }),
         { shadowRoot: false },
       )
-      customElements.define('ce-with-fallback-shadow-root-false', E)
+      customElements.define('ce-with-fallback-shadow-root-false-optimized', E)
 
       const Comp = defineComponent({
         render() {
           return (
             openBlock(),
-            createElementBlock('ce-with-fallback-shadow-root-false', null, [
-              this.$slots.foo
-                ? (openBlock(),
-                  createElementBlock('div', { key: 0, slot: 'foo' }, [
-                    renderSlot(this.$slots, 'foo'),
-                  ]))
-                : createCommentVNode('v-if', true),
-              renderSlot(this.$slots, 'default'),
-            ])
+            createElementBlock(
+              'ce-with-fallback-shadow-root-false-optimized',
+              null,
+              [
+                this.$slots.foo
+                  ? (openBlock(),
+                    createElementBlock('div', { key: 0, slot: 'foo' }, [
+                      renderSlot(this.$slots, 'foo'),
+                    ]))
+                  : createCommentVNode('v-if', true),
+                renderSlot(this.$slots, 'default'),
+              ],
+            )
           )
         },
       })
@@ -1290,27 +1388,107 @@ describe('defineCustomElement', () => {
       const app = createApp(App)
       app.mount(container)
       expect(container.innerHTML).toBe(
-        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+        `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
           `fallback` +
-          `</ce-with-fallback-shadow-root-false>`,
+          `</ce-with-fallback-shadow-root-false-optimized>`,
       )
 
       isShown.value = true
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+        `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
           `<div slot="foo">foo</div>` +
-          `</ce-with-fallback-shadow-root-false>`,
+          `</ce-with-fallback-shadow-root-false-optimized>`,
       )
 
       isShown.value = false
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<ce-with-fallback-shadow-root-false data-v-app="">` +
+        `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
           `fallback<!--v-if-->` +
-          `</ce-with-fallback-shadow-root-false>`,
+          `</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({
+          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 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="">` +
+            `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>`,
+        )
+      },
+    )
   })
 
   describe('helpers', () => {
index c5f3c9e341c4c88f95c0e212454e51189c5d541a..a6a4b1ba1e0df4a4dfbe1545f4fd31d1ec2bb9ee 100644 (file)
@@ -635,7 +635,7 @@ export class VueElement
       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
+      ;(n as any).$parentNode = n.parentNode
       if (remove) this.removeChild(n)
       n = next
     }
@@ -648,9 +648,12 @@ export class VueElement
     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++) {
       const o = outlets[i] as HTMLSlotElement
       const slotName = o.getAttribute('name') || 'default'
+      processedSlots.add(slotName)
       const content = this._slots![slotName]
       const parent = o.parentNode!
 
@@ -660,19 +663,7 @@ export class VueElement
       parent.insertBefore(anchor, o)
 
       if (content) {
-        for (const n of content) {
-          // for :slotted css
-          if (scopeId && n.nodeType === 1) {
-            const id = scopeId + '-s'
-            const walker = document.createTreeWalker(n, 1)
-            ;(n as Element).setAttribute(id, '')
-            let child
-            while ((child = walker.nextNode())) {
-              ;(child as Element).setAttribute(id, '')
-            }
-          }
-          parent.insertBefore(n, anchor)
-        }
+        insertSlottedContent(content, scopeId, parent, anchor)
       } else if (this._slotFallbacks) {
         const nodes = this._slotFallbacks[slotName]
         if (nodes) {
@@ -683,13 +674,25 @@ export class VueElement
       }
       parent.removeChild(o)
     }
+
+    // ensure default slot content is rendered if provided
+    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'),
+        )
+        insertSlottedContent(content, scopeId, this, this.firstChild)
+      }
+    }
   }
 
   /**
    * Only called when shadowRoot is false
    */
   _updateSlots(n1: VNode, n2: VNode): void {
-    // replace v-if nodes
+    // switch v-if nodes
     const prevNodes = collectNodes(n1.children as VNodeArrayChildren)
     const newNodes = collectNodes(n2.children as VNodeArrayChildren)
     for (let i = 0; i < prevNodes.length; i++) {
@@ -699,15 +702,10 @@ export class VueElement
         prevNode !== newNode &&
         (isComment(prevNode, 'v-if') || isComment(newNode, 'v-if'))
       ) {
-        Object.keys(this._slots!).forEach(name => {
-          const slotNodes = this._slots![name]
-          if (slotNodes) {
-            for (const node of slotNodes) {
-              if (node === prevNode) {
-                this._slots![name][i] = newNode
-                break
-              }
-            }
+        Object.entries(this._slots!).forEach(([_, nodes]) => {
+          const nodeIndex = nodes.indexOf(prevNode)
+          if (nodeIndex > -1) {
+            nodes[nodeIndex] = newNode
           }
         })
       }
@@ -815,6 +813,27 @@ export function useShadowRoot(): ShadowRoot | null {
   return el && el.shadowRoot
 }
 
+function insertSlottedContent(
+  content: Node[],
+  scopeId: string | undefined,
+  parent: ParentNode,
+  anchor: Node | null,
+) {
+  for (const n of content) {
+    // for :slotted css
+    if (scopeId && n.nodeType === 1) {
+      const id = scopeId + '-s'
+      const walker = document.createTreeWalker(n, 1)
+      ;(n as Element).setAttribute(id, '')
+      let child
+      while ((child = walker.nextNode())) {
+        ;(child as Element).setAttribute(id, '')
+      }
+    }
+    parent.insertBefore(n, anchor)
+  }
+}
+
 function collectFragmentNodes(child: VNode): Node[] {
   return [
     child.el as Node,