fix(runtime-vapor): component emits vdom interop (#13498)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details

This commit is contained in:
edison 2025-07-16 13:49:52 +08:00 committed by GitHub
parent 5ab938db50
commit d95fc186c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 39 additions and 6 deletions

View File

@ -26,7 +26,27 @@ describe('vdomInterop', () => {
})
})
describe.todo('emit', () => {})
describe('emit', () => {
test('emit from vapor child to vdom parent', () => {
const VaporChild = defineVaporComponent({
emits: ['click'],
setup(_, { emit }) {
emit('click')
return []
},
})
const fn = vi.fn()
define({
setup() {
return () => h(VaporChild as any, { onClick: fn })
},
}).render()
// fn should be called once
expect(fn).toHaveBeenCalledTimes(1)
})
})
describe('slots', () => {
test('basic', () => {

View File

@ -1,7 +1,8 @@
import { type ObjectEmitsOptions, baseEmit } from '@vue/runtime-dom'
import type { VaporComponent, VaporComponentInstance } from './component'
import { EMPTY_OBJ, hasOwn, isArray } from '@vue/shared'
import { resolveSource } from './componentProps'
import { type RawProps, resolveSource } from './componentProps'
import { interopKey } from './vdomInterop'
/**
* The logic from core isn't too reusable so it's better to duplicate here
@ -40,13 +41,17 @@ export function emit(
)
}
function propGetter(rawProps: Record<string, any>, key: string) {
function propGetter(rawProps: RawProps, key: string) {
const dynamicSources = rawProps.$
if (dynamicSources) {
let i = dynamicSources.length
while (i--) {
const source = resolveSource(dynamicSources[i])
if (hasOwn(source, key)) return resolveSource(source[key])
if (hasOwn(source, key))
// for props passed from VDOM component, no need to resolve
return dynamicSources[interopKey]
? source[key]
: resolveSource(source[key])
}
}
return rawProps[key] && resolveSource(rawProps[key])

View File

@ -23,10 +23,11 @@ import {
import { ReactiveFlags } from '@vue/reactivity'
import { normalizeEmitsOptions } from './componentEmits'
import { renderEffect } from './renderEffect'
import type { interopKey } from './vdomInterop'
export type RawProps = Record<string, () => unknown> & {
// generated by compiler for :[key]="x" or v-bind="x"
$?: DynamicPropsSource[]
$?: DynamicPropsSource[] & { [interopKey]?: boolean }
}
export type DynamicPropsSource =

View File

@ -37,6 +37,8 @@ import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
export const interopKey: unique symbol = Symbol(`interop`)
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<
VaporInteropInterface,
@ -51,11 +53,16 @@ const vaporInteropImpl: Omit<
const propsRef = shallowRef(vnode.props)
const slotsRef = shallowRef(vnode.children)
const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
() => propsRef.value,
]
// mark as interop props
dynamicPropSource[interopKey] = true
// @ts-expect-error
const instance = (vnode.component = createComponent(
vnode.type as any as VaporComponent,
{
$: [() => propsRef.value],
$: dynamicPropSource,
} as RawProps,
{
_: slotsRef, // pass the slots ref