]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): properly handle inherit transition during clone VNode (#10809)
authoredison <daiwei521@126.com>
Mon, 29 Apr 2024 06:39:14 +0000 (14:39 +0800)
committerGitHub <noreply@github.com>
Mon, 29 Apr 2024 06:39:14 +0000 (14:39 +0800)
close #3716
close #10497
close #4091

packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/vnode.ts
packages/vue/__tests__/e2e/Transition.spec.ts

index 3044884669a3a21fb91c2b08999918d682173d79..eb9b896c7f91d84c7de2d46b905e66f17ce44d8a 100644 (file)
@@ -166,7 +166,7 @@ export function renderComponentRoot(
             propsOptions,
           )
         }
-        root = cloneVNode(root, fallthroughAttrs)
+        root = cloneVNode(root, fallthroughAttrs, false, true)
       } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
         const allAttrs = Object.keys(attrs)
         const eventAttrs: string[] = []
@@ -221,10 +221,15 @@ export function renderComponentRoot(
           getComponentName(instance.type),
         )
       }
-      root = cloneVNode(root, {
-        class: cls,
-        style: style,
-      })
+      root = cloneVNode(
+        root,
+        {
+          class: cls,
+          style: style,
+        },
+        false,
+        true,
+      )
     }
   }
 
@@ -237,7 +242,7 @@ export function renderComponentRoot(
       )
     }
     // clone before mutating since the root may be a hoisted vnode
-    root = cloneVNode(root)
+    root = cloneVNode(root, null, false, true)
     root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
   }
   // inherit transition data
index a1a6a908d2ae116a6eb2931ad74c1a7814cbf877..0e2a4bafcc5ae6d69cc7978a58dc10f30b69c15c 100644 (file)
@@ -624,10 +624,11 @@ export function cloneVNode<T, U>(
   vnode: VNode<T, U>,
   extraProps?: (Data & VNodeProps) | null,
   mergeRef = false,
+  cloneTransition = false,
 ): VNode<T, U> {
   // This is intentionally NOT using spread or extend to avoid the runtime
   // key enumeration cost.
-  const { props, ref, patchFlag, children } = vnode
+  const { props, ref, patchFlag, children, transition } = vnode
   const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
   const cloned: VNode<T, U> = {
     __v_isVNode: true,
@@ -670,7 +671,7 @@ export function cloneVNode<T, U>(
     dynamicChildren: vnode.dynamicChildren,
     appContext: vnode.appContext,
     dirs: vnode.dirs,
-    transition: vnode.transition,
+    transition,
 
     // These should technically only be non-null on mounted VNodes. However,
     // they *should* be copied for kept-alive vnodes. So we just always copy
@@ -685,9 +686,18 @@ export function cloneVNode<T, U>(
     ctx: vnode.ctx,
     ce: vnode.ce,
   }
+
+  // if the vnode will be replaced by the cloned one, it is necessary
+  // to clone the transition to ensure that the vnode referenced within
+  // the transition hooks is fresh.
+  if (transition && cloneTransition) {
+    cloned.transition = transition.clone(cloned as VNode)
+  }
+
   if (__COMPAT__) {
     defineLegacyVNodeProperties(cloned as VNode)
   }
+
   return cloned
 }
 
index b2c1ba572dc4a90a92d17b322c07efef0e4dbcb4..4fe78ae8ab0d7e37ccf3cdde1e900b000174a0c5 100644 (file)
@@ -1215,6 +1215,54 @@ describe('e2e: Transition', () => {
       E2E_TIMEOUT,
     )
 
+    // #3716
+    test(
+      'wrapping transition + fallthrough attrs',
+      async () => {
+        await page().goto(baseUrl)
+        await page().waitForSelector('#app')
+        await page().evaluate(() => {
+          const { createApp, ref } = (window as any).Vue
+          createApp({
+            components: {
+              'my-transition': {
+                template: `
+                  <transition foo="1" name="test">
+                    <slot></slot>
+                  </transition>
+                `,
+              },
+            },
+            template: `
+            <div id="container">
+              <my-transition>
+                <div v-if="toggle">content</div>
+              </my-transition>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            setup: () => {
+              const toggle = ref(true)
+              const click = () => (toggle.value = !toggle.value)
+              return { toggle, click }
+            },
+          }).mount('#app')
+        })
+        expect(await html('#container')).toBe('<div foo="1">content</div>')
+
+        await click('#toggleBtn')
+        // toggle again before leave finishes
+        await nextTick()
+        await click('#toggleBtn')
+
+        await transitionFinish()
+        expect(await html('#container')).toBe(
+          '<div foo="1" class="">content</div>',
+        )
+      },
+      E2E_TIMEOUT,
+    )
+
     test(
       'w/ KeepAlive + unmount innerChild',
       async () => {