Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ export function ssrTransformTransitionGroup(
return (): void => {
const tag = findProp(node, 'tag')
if (tag) {
const otherProps = node.props.filter(p => p !== tag)
// 在处理 TransitionGroup 的属性时,过滤掉 name/tag 等私有 props
const otherProps = node.props.filter(p => {
// 排除 tag(已单独处理)和 name(私有 props,不该透传)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

中文注释不太好,其他贡献者可能看不懂。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry 这边发现还有其他属性没有处理
image
我需要晚上回家处理下。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

中文注释不太好,其他贡献者可能看不懂。

谢谢您的督促,已经调整好了。

if (
p === tag ||
(p.type === NodeTypes.ATTRIBUTE && p.name === 'name')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to only fix the problem for name, not for other props such as duration.

Also, checking for a type of ATTRIBUTE will only find static attributes. Attributes bound with v-bind, e.g. :name="n", will still be included.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to only fix the problem for name, not for other props such as duration.

Also, checking for a type of ATTRIBUTE will only find static attributes. Attributes bound with v-bind, e.g. :name="n", will still be included.

image You're absolutely right. I only noticed this issue after fixing the previous one. I'll figure out how to address it—it seems like this isn't the right place to handle it, so I'll try another approach. Thank you for your oversight.

) {
return false
}
return true
})
const { props, directives } = buildProps(
node,
context,
Expand Down
18 changes: 15 additions & 3 deletions packages/runtime-dom/src/components/TransitionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
useTransitionState,
warn,
} from '@vue/runtime-core'
import { extend } from '@vue/shared'
import { extend, hasOwn } from '@vue/shared'

const positionMap = new WeakMap<VNode, DOMRect>()
const newPositionMap = new WeakMap<VNode, DOMRect>()
Expand Down Expand Up @@ -130,6 +130,19 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
tag = 'span'
}

// Filter out transition-specific props and TransitionGroup-specific props
// to avoid invalid HTML attributes
const filteredProps: Record<string, any> = {}
for (const key in rawProps) {
if (
!hasOwn(TransitionPropsValidators, key) &&
key !== 'tag' &&
key !== 'moveClass'
) {
filteredProps[key] = (rawProps as any)[key]
}
}

prevChildren = []
if (children) {
for (let i = 0; i < children.length; i++) {
Expand Down Expand Up @@ -166,8 +179,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
warn(`<TransitionGroup> children must be keyed.`)
}
}

return createVNode(tag, null, children)
return createVNode(tag, tag === Fragment ? null : filteredProps, children)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused. This seems to change the props from null to filteredProps. If the old value was null then it doesn't seem this is where the spurious props were being applied originally. I'm not sure how passing extra props here would help.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the fix isn't changing from null to filteredProps - it's changing
from passing all props (including invalid HTML attributes like
transition props) to passing only valid HTML attributes.

The condition tag === Fragment ? null : filteredProps means:

  • If rendering a Fragment: pass null (no props needed)
  • If rendering an actual HTML element: pass only the filtered, valid
    HTML props

This prevents invalid HTML attributes like name="fade" or
duration="300" from appearing on the DOM element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing null. The new code passes more props, not fewer.

I believe the changes to this file are incorrect and should be reverted.

Copy link
Author

@ZKunZhang ZKunZhang Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing null. The new code passes more props, not fewer.

I believe the changes to this file are incorrect and should be reverted.

Thank you for your guidance, and this line of code has awakened me to the issue — I've identified a fundamental flaw in my previous approach to fixing the issue. The core problem is that Vue's automatic fallthrough mechanism fails to properly handle the declared props of TransitionGroup, causing properties that should be filtered by the component to erroneously appear in the final HTML. The runtime fallthrough mechanism malfunctions, resulting in transition-related attributes (such as name="fade") being incorrectly rendered into the HTML. The same issue occurs in SSR environments, generating HTML with invalid attributes. Therefore, my previous method of manually filtering attributes within each component was incorrect.

There is a critical flaw in how TransitionGroup handles props:

  1. A dynamic deletion operation delete t.props.mode is executed in the decorate function
  2. This breaks the fallthrough mechanism: Vue's setFullProps function relies on hasOwn(options, camelKey) to determine which properties are declared props
  3. The end result: The deleted mode and all other transition properties fail to be correctly identified as declared props, causing them to erroneously enter the attrs object

The correct architecture-level fix should be:

  • Rebuild the props definition for TransitionGroup
  • Use extend instead of object spreading (to meet ESBuild requirements)
  • Exclude mode during the definition phase to avoid subsequent deletion operations

It's important to note that during SSR compilation, all attributes are directly compiled into the generated code. Since SSR is processed at compile time rather than runtime, the runtime fallthrough mechanism does not take effect during SSR compilation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the original code is this:

return createVNode(tag, null, children)

That isn't passing all props, it's passing . The new code passes more props, not fewer.null

I believe the changes to this file are incorrect and should be reverted.

I double-checked and realized I had indeed misidentified the root cause of the issue. This led me to fix a file that didn’t need fixing, but I have now reverted that change. 😣

}
},
})
Expand Down