Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions packages/runtime-vapor/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import {
createApp,
currentInstance,
defineComponent,
h,
inject,
isShallow,
markRaw,
Expand Down Expand Up @@ -2606,4 +2608,38 @@ describe('component: props', () => {
app.unmount()
})
})

test('empty fallthrough attrs are not merged into the root props', async () => {
const child = `<script setup>
defineProps(['style'])
</script><template><i>{{ typeof $props.style }}</i></template>`
const VdomChild = defineComponent({
props: ['style'],
setup: props => () => h('i', typeof props.style),
})
const wrapper = (child: string, bind = '') => `<script setup>
const data = _data
const components = _components
defineEmits(['ready'])
</script><template><components.${child} :style="data.style" ${bind} /></template>`
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: `<template>
<components.A @ready="() => {}" />
<components.B @ready="() => {}" />
<components.C @ready="() => {}" />
</template>`,
},
() => ref({ style: undefined, rest: {} }),
() => {},
{ VdomChild },
)
expect(vdom.after).toBe('<i>undefined</i><i>undefined</i><i>object</i>')
expect(vapor.after).toBe(vdom.after)
})
})
13 changes: 9 additions & 4 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
type DynamicPropsSource,
INITIAL_RAW_VALUES,
type RawProps,
fallthroughOnlyKey,
getPropsProxyHandlers,
getStaticBindingKeys,
hasFallthroughAttrs,
Expand Down Expand Up @@ -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
}
}

Expand Down
19 changes: 16 additions & 3 deletions packages/runtime-vapor/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> & {
// 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 =
Expand Down Expand Up @@ -715,14 +721,21 @@ export function collectProps(
// Raw attrs may contain prototype-named keys such as __proto__.
const rawValues: Record<string, any> = 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<string, unknown>)()
: 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) {
Expand All @@ -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)
}
Expand Down
Loading