Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime-core): properly handle inherit transition during clone VNode #10809

Merged
merged 12 commits into from
Apr 29, 2024
17 changes: 11 additions & 6 deletions packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -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,
)
}
}

Expand All @@ -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
Expand Down
14 changes: 12 additions & 2 deletions packages/runtime-core/src/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
48 changes: 48 additions & 0 deletions packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down