diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap index 2d64e1ffe52..b84e6aef6c5 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -67,7 +67,7 @@ exports[`compiler: template ref transform > static ref (inline mode) 1`] = ` " const _setTemplateRef = _createTemplateRefSetter() const n0 = t0() - _setTemplateRef(n0, foo) + _setTemplateRef(n0, foo, null, null, "foo") return n0 " `; diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts index 2c883d10cc6..4a1d011c178 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -55,8 +55,8 @@ describe('compiler: template ref transform', () => { bindingMetadata: { foo: BindingTypes.SETUP_REF }, }) expect(code).matchSnapshot() - // pass the actual ref - expect(code).contains('_setTemplateRef(n0, foo)') + // pass the actual ref and ref key + expect(code).contains('_setTemplateRef(n0, foo, null, null, "foo")') }) test('dynamic ref', () => { diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts index af8facc57b1..3aa037a0458 100644 --- a/packages/compiler-vapor/src/generators/templateRef.ts +++ b/packages/compiler-vapor/src/generators/templateRef.ts @@ -10,15 +10,17 @@ export function genSetTemplateRef( oper: SetTemplateRefIRNode, context: CodegenContext, ): CodeFragment[] { + const [refValue, refKey] = genRefValue(oper.value, context) return [ NEWLINE, oper.effect && `r${oper.element} = `, ...genCall( setTemplateRefIdent, // will be generated in root scope `n${oper.element}`, - genRefValue(oper.value, context), + refValue, oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined, oper.refFor && 'true', + refKey, ), ] } @@ -38,8 +40,8 @@ function genRefValue(value: SimpleExpressionNode, context: CodegenContext) { binding === BindingTypes.SETUP_REF || binding === BindingTypes.SETUP_MAYBE_REF ) { - return [value.content] + return [[value.content], JSON.stringify(value.content)] } } - return genExpression(value, context) + return [genExpression(value, context)] } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 243bde548c5..311a3b55961 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -558,6 +558,14 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { setRef } from './rendererTemplateRef' +/** + * @internal + */ +export { type VNodeNormalizedRef, normalizeRef } from './vnode' /** * @internal */ diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index 31fcf8c2d5b..20fe776d8cd 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -75,7 +75,7 @@ export function setRef( const setupState = owner.setupState const rawSetupState = toRaw(setupState) const canSetSetupRef = - setupState === EMPTY_OBJ + setupState === undefined || setupState === EMPTY_OBJ ? () => false : (key: string) => { if (__DEV__) { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 4b31151da22..6addb5151ed 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -454,18 +454,17 @@ const createVNodeWithArgsTransform = ( const normalizeKey = ({ key }: VNodeProps): VNode['key'] => key != null ? key : null -const normalizeRef = ({ - ref, - ref_key, - ref_for, -}: VNodeProps): VNodeNormalizedRefAtom | null => { +export const normalizeRef = ( + { ref, ref_key, ref_for }: VNodeProps, + i: ComponentInternalInstance = currentRenderingInstance!, +): VNodeNormalizedRefAtom | null => { if (typeof ref === 'number') { ref = '' + ref } return ( ref != null ? isString(ref) || isRef(ref) || isFunction(ref) - ? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for } + ? { i, r: ref, k: ref_key, f: !!ref_for } : ref : null ) as any diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index d1ede2a6c9a..dac40493381 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -2,6 +2,10 @@ import { createVaporApp, vaporInteropPlugin } from '../src' import { type App, type Component, createApp } from '@vue/runtime-dom' import type { VaporComponent, VaporComponentInstance } from '../src/component' import type { RawProps } from '../src/componentProps' +import { compileScript, parse } from '@vue/compiler-sfc' +import * as runtimeVapor from '../src' +import * as runtimeDom from '@vue/runtime-dom' +import * as VueServerRenderer from '@vue/server-renderer' export interface RenderContext { component: VaporComponent @@ -135,3 +139,50 @@ export function makeInteropRender(): (comp: Component) => InteropRenderContext { return define } + +export { runtimeDom, runtimeVapor, VueServerRenderer } +export function compile( + sfc: string, + data: runtimeDom.Ref, + components: Record = {}, + { + vapor = true, + ssr = false, + }: { + vapor?: boolean | undefined + ssr?: boolean | undefined + } = {}, +): any { + if (!sfc.includes(`const data = _data; const components = _components;` + + sfc + } + const descriptor = parse(sfc).descriptor + + const script = compileScript(descriptor, { + id: 'x', + isProd: true, + inlineTemplate: true, + genDefaultAs: '__sfc__', + vapor, + templateOptions: { + ssr, + }, + }) + + const code = + script.content + .replace(/\bimport {/g, 'const {') + .replace(/ as _/g, ': _') + .replace(/} from ['"]vue['"]/g, `} = Vue`) + .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') + + '\nreturn __sfc__' + + return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)( + { ...runtimeDom, ...runtimeVapor }, + VueServerRenderer, + data, + components, + ) +} diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts index f1ce23ac156..1695a2e402c 100644 --- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts @@ -8,11 +8,12 @@ import { createSlot, createTemplateRefSetter, defineVaporComponent, + delegateEvents, insert, renderEffect, template, } from '../../src' -import { makeRender } from '../_utils' +import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils' import { type ShallowRef, currentInstance, @@ -753,3 +754,227 @@ describe('api: template ref', () => { // expect(elRef1.value).toBe(elRef2.value) // }) }) + +describe('interop: template ref', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + + const triggerEvent = (type: string, el: Element) => { + const event = new Event(type, { bubbles: true }) + el.dispatchEvent(event) + } + + delegateEvents('click') + + async function testTemplateRefInterop( + code: string, + components: Record = {}, + data: any = {}, + { vapor = false } = {}, + ) { + const clientComponents: any = {} + for (const key in components) { + const comp = components[key] + const code = comp.code + const isVaporComp = !!comp.vapor + clientComponents[key] = compile(code, data, clientComponents, { + vapor: isVaporComp, + }) + } + + const clientComp = compile(code, data, clientComponents, { + vapor, + }) + + const app = (vapor ? runtimeVapor.createVaporApp : runtimeDom.createApp)( + clientComp, + ) + app.use(runtimeVapor.vaporInteropPlugin) + + const container = document.createElement('div') + document.body.appendChild(container) + app.mount(container) + return { container } + } + + test('vdom app: useTemplateRef with vapor child', async () => { + const { container } = await testTemplateRefInterop( + ` + `, + { + VaporChild: { + code: ` + + + `, + vapor: true, + }, + }, + ) + + expect(container.innerHTML).toBe( + `
foo
`, + ) + + const btn = container.querySelector('.btn') + triggerEvent('click', btn!) + await nextTick() + expect(container.innerHTML).toBe( + `
bar
`, + ) + }) + + test('vdom app: static ref with vapor child', async () => { + const { container } = await testTemplateRefInterop( + ` + `, + { + VaporChild: { + code: ` + + + `, + vapor: true, + }, + }, + ) + + expect(container.innerHTML).toBe( + `
foo
`, + ) + + const btn = container.querySelector('.btn') + triggerEvent('click', btn!) + await nextTick() + expect(container.innerHTML).toBe( + `
bar
`, + ) + }) + + test('vapor app: useTemplateRef with vdom child', async () => { + const { container } = await testTemplateRefInterop( + ` + `, + { + VDOMChild: { + code: ` + + + `, + vapor: false, + }, + }, + undefined, + { vapor: true }, + ) + + expect(container.innerHTML).toBe( + `
foo
`, + ) + + const btn = container.querySelector('.btn') + triggerEvent('click', btn!) + await nextTick() + expect(container.innerHTML).toBe( + `
bar
`, + ) + }) + + test('vapor app: static ref with vdom child', async () => { + const { container } = await testTemplateRefInterop( + ` + `, + { + VDomChild: { + code: ` + + + `, + vapor: false, + }, + }, + undefined, + { vapor: true }, + ) + + expect(container.innerHTML).toBe( + `
foo
`, + ) + + const btn = container.querySelector('.btn') + triggerEvent('click', btn!) + await nextTick() + expect(container.innerHTML).toBe( + `
bar
`, + ) + }) +}) diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 6ba2bf895fb..72d3fe27d64 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1,51 +1,6 @@ import { createVaporSSRApp, delegateEvents } from '../src' import { nextTick, ref } from '@vue/runtime-dom' -import { compileScript, parse } from '@vue/compiler-sfc' -import * as runtimeVapor from '../src' -import * as runtimeDom from '@vue/runtime-dom' -import * as VueServerRenderer from '@vue/server-renderer' - -const Vue = { ...runtimeDom, ...runtimeVapor } - -function compile( - sfc: string, - data: runtimeDom.Ref, - components: Record = {}, - ssr = false, -) { - if (!sfc.includes(`const data = _data; const components = _components;` + - sfc - } - const descriptor = parse(sfc).descriptor - - const script = compileScript(descriptor, { - id: 'x', - isProd: true, - inlineTemplate: true, - genDefaultAs: '__sfc__', - vapor: true, - templateOptions: { - ssr, - }, - }) - - const code = - script.content - .replace(/\bimport {/g, 'const {') - .replace(/ as _/g, ': _') - .replace(/} from ['"]vue['"]/g, `} = Vue`) - .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') + - '\nreturn __sfc__' - - return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)( - Vue, - VueServerRenderer, - data, - components, - ) -} +import { VueServerRenderer, compile, runtimeDom } from './_utils' async function testHydration( code: string, @@ -56,10 +11,12 @@ async function testHydration( const clientComponents: any = {} for (const key in components) { clientComponents[key] = compile(components[key], data, clientComponents) - ssrComponents[key] = compile(components[key], data, ssrComponents, true) + ssrComponents[key] = compile(components[key], data, ssrComponents, { + ssr: true, + }) } - const serverComp = compile(code, data, ssrComponents, true) + const serverComp = compile(code, data, ssrComponents, { ssr: true }) const html = await VueServerRenderer.renderToString( runtimeDom.createSSRApp(serverComp), ) diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index 7a30d219811..887437ed71a 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -20,9 +20,12 @@ import { isString, remove, } from '@vue/shared' -import { DynamicFragment } from './block' +import { DynamicFragment, isFragment } from './block' -export type NodeRef = string | Ref | ((ref: Element) => void) +export type NodeRef = + | string + | Ref + | ((ref: Element | VaporComponentInstance, refs: Record) => void) export type RefEl = Element | VaporComponentInstance export type setRefFn = ( @@ -46,9 +49,16 @@ export function setRef( ref: NodeRef, oldRef?: NodeRef, refFor = false, + refKey?: string, ): NodeRef | undefined { if (!instance || instance.isUnmounted) return + // vdom interop + if (isFragment(el) && el.setRef) { + el.setRef(instance, ref, refFor, refKey) + return + } + const setupState: any = __DEV__ ? instance.setupState || {} : null const refValue = getRefValue(el) @@ -105,6 +115,7 @@ export function setRef( } } else { ref.value = existing + if (refKey) refs[refKey] = existing } } else if (!existing.includes(refValue)) { existing.push(refValue) @@ -116,6 +127,7 @@ export function setRef( } } else if (_isRef) { ref.value = refValue + if (refKey) refs[refKey] = refValue } else if (__DEV__) { warn('Invalid template ref type:', ref, `(${typeof ref})`) } @@ -134,6 +146,7 @@ export function setRef( } } else if (_isRef) { ref.value = null + if (refKey) refs[refKey] = null } }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index e021ce84b05..564b56d5b73 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -8,6 +8,7 @@ import { import { createComment, createTextNode } from './dom/node' import { EffectScope, setActiveSub } from '@vue/reactivity' import { isHydrating } from './dom/hydration' +import type { NodeRef } from './apiTemplateRef' export type Block = | Node @@ -23,6 +24,12 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + setRef?: ( + instance: VaporComponentInstance, + ref: NodeRef, + refFor: boolean, + refKey: string | undefined, + ) => void constructor(nodes: Block) { this.nodes = nodes diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 1573a306922..22b20af4764 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -8,17 +8,20 @@ import { type ShallowRef, type Slots, type VNode, + type VNodeNormalizedRef, type VaporInteropInterface, createInternalObject, createVNode, currentInstance, ensureRenderer, isEmitListener, + normalizeRef, onScopeDispose, renderSlot, shallowReactive, shallowRef, simpleSetCurrentInstance, + setRef as vdomSetRef, } from '@vue/runtime-dom' import { type LooseRawProps, @@ -30,12 +33,13 @@ import { unmountComponent, } from './component' import { type Block, VaporFragment, insert, remove } from './block' -import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' +import { EMPTY_OBJ, extend, isFunction, isReservedProp } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import type { NodeRef } from './apiTemplateRef' export const interopKey: unique symbol = Symbol(`interop`) @@ -50,7 +54,15 @@ const vaporInteropImpl: Omit< const prev = currentInstance simpleSetCurrentInstance(parentComponent) - const propsRef = shallowRef(vnode.props) + // filter out reserved props + const props: VNode['props'] = {} + for (const key in vnode.props) { + if (!isReservedProp(key)) { + props[key] = vnode.props[key] + } + } + + const propsRef = shallowRef(props) const slotsRef = shallowRef(vnode.children) const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [ @@ -187,9 +199,12 @@ function createVDOMComponent( : new Proxy(wrapper.slots, vaporSlotsProxyHandler) } + let rawRef: VNodeNormalizedRef | null = null let isMounted = false const parentInstance = currentInstance as VaporComponentInstance const unmount = (parentNode?: ParentNode) => { + // unset ref + if (rawRef) vdomSetRef(rawRef, null, null, vnode, true) internals.umt(vnode.component!, null, !!parentNode) } @@ -204,6 +219,8 @@ function createVDOMComponent( undefined, false, ) + // set ref + if (rawRef) vdomSetRef(rawRef, null, null, vnode) onScopeDispose(unmount, true) isMounted = true } else { @@ -220,6 +237,22 @@ function createVDOMComponent( frag.remove = unmount + frag.setRef = ( + instance: VaporComponentInstance, + ref: NodeRef, + refFor: boolean, + refKey: string | undefined, + ): void => { + rawRef = normalizeRef( + { + ref: ref as any, + ref_for: refFor, + ref_key: refKey, + }, + instance as any, + ) + } + return frag }