diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts
index 9a0383fa21f..d4ce8fc47eb 100644
--- a/packages/runtime-vapor/__tests__/componentProps.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts
@@ -3,6 +3,8 @@
import {
createApp,
currentInstance,
+ defineComponent,
+ h,
inject,
isShallow,
markRaw,
@@ -2606,4 +2608,38 @@ describe('component: props', () => {
app.unmount()
})
})
+
+ test('empty fallthrough attrs are not merged into the root props', async () => {
+ const child = `{{ typeof $props.style }}`
+ const VdomChild = defineComponent({
+ props: ['style'],
+ setup: props => () => h('i', typeof props.style),
+ })
+ const wrapper = (child: string, bind = '') => ``
+ const { vdom, vapor } = await renderParity(
+ {
+ Child: child,
+ A: wrapper('Child'),
+ B: wrapper('VdomChild'),
+ // an explicit v-bind still merges, like mergeProps
+ C: wrapper('Child', 'v-bind="data.rest"'),
+ App: `
+ {}" />
+ {}" />
+ {}" />
+ `,
+ },
+ () => ref({ style: undefined, rest: {} }),
+ () => {},
+ { VdomChild },
+ )
+ expect(vdom.after).toBe('undefinedundefinedobject')
+ expect(vapor.after).toBe(vdom.after)
+ })
})
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 809f790990f..c7e2f83f325 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -88,6 +88,7 @@ import {
type DynamicPropsSource,
INITIAL_RAW_VALUES,
type RawProps,
+ fallthroughOnlyKey,
getPropsProxyHandlers,
getStaticBindingKeys,
hasFallthroughAttrs,
@@ -366,11 +367,15 @@ export function createComponent(
// must see exactly one fallthrough source
if (rawProps && rawProps !== EMPTY_OBJ) {
const sources = (rawProps as RawProps).$
- rawProps = extend({}, rawProps, {
- $: sources ? sources.concat(source) : [source],
- }) as RawProps
+ rawProps = extend(
+ {},
+ rawProps,
+ sources
+ ? { $: sources.concat(source) }
+ : { $: [source], [fallthroughOnlyKey]: true },
+ ) as RawProps
} else {
- rawProps = { $: [source] } as RawProps
+ rawProps = { $: [source], [fallthroughOnlyKey]: true } as RawProps
}
}
diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts
index c4ceada475d..1600ac75613 100644
--- a/packages/runtime-vapor/src/componentProps.ts
+++ b/packages/runtime-vapor/src/componentProps.ts
@@ -49,11 +49,17 @@ import { normalizeEmitsOptions } from './componentEmits'
import { RenderEffect } from './renderEffect'
import { interopKey, isInteropEnabled } from './vdomInteropState'
+export const fallthroughOnlyKey: unique symbol = Symbol(
+ __DEV__ ? 'fallthrough only' : '',
+)
+
export type RawProps = Record & {
// generated by compiler for :[key]="x" or v-bind="x"
$?: DynamicPropsSource[]
// evaluated by vdom, which patches the values in later
[interopKey]?: boolean
+ // the only dynamic source is the injected fallthrough attrs
+ [fallthroughOnlyKey]?: boolean
}
export type DynamicPropsSource =
@@ -715,14 +721,21 @@ export function collectProps(
// Raw attrs may contain prototype-named keys such as __proto__.
const rawValues: Record = Object.create(null)
const dynamicSources = rawProps.$ || EMPTY_ARR
+ // like vdom, which merges fallthrough attrs only when there are some
+ const attrs = rawProps[fallthroughOnlyKey]
+ ? (dynamicSources[0] as () => Record)()
+ : undefined
+ const isMerging = attrs
+ ? Object.keys(attrs).length > 0
+ : dynamicSources.length > 0
for (let i = -1; i < dynamicSources.length; i++) {
const source = i < 0 ? rawProps : dynamicSources[i]
const isDynamic = isFunction(source)
- const resolved = isDynamic ? source() : source
+ const resolved = attrs && i === 0 ? attrs : isDynamic ? source() : source
for (const key in resolved) {
if (i < 0 && key === '$') continue
const value = isDynamic ? resolved[key] : readSource(resolved[key])
- if (!dynamicSources.length) {
+ if (!isMerging) {
rawValues[key] = value
} else if (key === 'class') {
if (rawValues.class !== value) {
@@ -749,7 +762,7 @@ export function collectProps(
}
}
// merged sources are normalized above
- if (dynamicSources.length) {
+ if (isMerging) {
if (rawValues.style) {
rawValues.style = stabilizeDynamicSourceValue(prevStyle, rawValues.style)
}