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 f2eade4bcdf..7efbe45f2b9 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
@@ -43,6 +43,15 @@ export function render(_ctx) {
}"
`;
+exports[`compiler: template ref transform > static ref (inline mode) 1`] = `
+"
+ const _setTemplateRef = _createTemplateRefSetter()
+ const n0 = t0()
+ _setTemplateRef(n0, foo, null, null, "foo")
+ return n0
+"
+`;
+
exports[`compiler: template ref transform > static ref 1`] = `
"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
const t0 = _template("
", true)
diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
index 6be8f18779c..65bfaeef102 100644
--- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
@@ -1,3 +1,4 @@
+import { BindingTypes } from '@vue/compiler-dom'
import {
DynamicFlag,
type ForIRNode,
@@ -48,6 +49,16 @@ describe('compiler: template ref transform', () => {
expect(code).contains('_setTemplateRef(n0, "foo")')
})
+ test('static ref (inline mode)', () => {
+ const { code } = compileWithTransformRef(``, {
+ inline: true,
+ bindingMetadata: { foo: BindingTypes.SETUP_REF },
+ })
+ expect(code).matchSnapshot()
+ // pass the actual ref and ref key
+ expect(code).contains('_setTemplateRef(n0, foo, null, null, "foo")')
+ })
+
test('dynamic ref', () => {
const { ir, code } = compileWithTransformRef(``)
diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts
index a4d6d546ed3..3aa037a0458 100644
--- a/packages/compiler-vapor/src/generators/templateRef.ts
+++ b/packages/compiler-vapor/src/generators/templateRef.ts
@@ -2,6 +2,7 @@ import { genExpression } from './expression'
import type { CodegenContext } from '../generate'
import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir'
import { type CodeFragment, NEWLINE, genCall } from './utils'
+import { BindingTypes, type SimpleExpressionNode } from '@vue/compiler-dom'
export const setTemplateRefIdent = `_setTemplateRef`
@@ -9,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}`,
- genExpression(oper.value, context),
+ refValue,
oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined,
oper.refFor && 'true',
+ refKey,
),
]
}
@@ -25,3 +28,20 @@ export function genSetTemplateRef(
export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] {
return [NEWLINE, `let r${oper.id}`]
}
+
+function genRefValue(value: SimpleExpressionNode, context: CodegenContext) {
+ // in inline mode there is no setupState object, so we can't use string
+ // keys to set the ref. Instead, we need to transform it to pass the
+ // actual ref instead.
+ if (!__BROWSER__ && value && context.options.inline) {
+ const binding = context.options.bindingMetadata[value.content]
+ if (
+ binding === BindingTypes.SETUP_LET ||
+ binding === BindingTypes.SETUP_REF ||
+ binding === BindingTypes.SETUP_MAYBE_REF
+ ) {
+ return [[value.content], JSON.stringify(value.content)]
+ }
+ }
+ return [genExpression(value, context)]
+}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index e309554f2f6..e30bd5abc1a 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -557,3 +557,11 @@ export { startMeasure, endMeasure } from './profiling'
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { setRef } from './rendererTemplateRef'
+/**
+ * @internal
+ */
+export { type VNodeNormalizedRef, normalizeRef } from './vnode'
diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts
index ca21030dc35..8b5dbe56e37 100644
--- a/packages/runtime-core/src/rendererTemplateRef.ts
+++ b/packages/runtime-core/src/rendererTemplateRef.ts
@@ -76,7 +76,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 0ed64554478..729d42de78c 100644
--- a/packages/runtime-vapor/__tests__/_utils.ts
+++ b/packages/runtime-vapor/__tests__/_utils.ts
@@ -2,6 +2,10 @@ import { createVaporApp } from '../src'
import type { App } 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
@@ -82,3 +86,50 @@ export function makeRender(
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(`` +
+ 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 e696fadd469..1073389b974 100644
--- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
+++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
@@ -5,11 +5,12 @@ import {
createIf,
createSlot,
createTemplateRefSetter,
+ delegateEvents,
insert,
renderEffect,
template,
} from '../../src'
-import { makeRender } from '../_utils'
+import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils'
import {
type ShallowRef,
currentInstance,
@@ -716,3 +717,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: `
+
+ {{msg}}
+ `,
+ 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: `
+
+ {{msg}}
+ `,
+ 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: `
+
+ {{msg}}
+ `,
+ 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: `
+
+ {{msg}}
+ `,
+ 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(`` +
- 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 c5a6c5fb2b6..5c2e6ca0205 100644
--- a/packages/runtime-vapor/src/apiTemplateRef.ts
+++ b/packages/runtime-vapor/src/apiTemplateRef.ts
@@ -20,8 +20,12 @@ import {
isString,
remove,
} from '@vue/shared'
+import { 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 = (
@@ -45,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 = isVaporComponent(el) ? getExposed(el) || el : el
@@ -104,6 +115,7 @@ export function setRef(
}
} else {
ref.value = existing
+ if (refKey) refs[refKey] = existing
}
} else if (!existing.includes(refValue)) {
existing.push(refValue)
@@ -115,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 +147,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 b782afd38d3..f27cb5b59e5 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, pauseTracking, resetTracking } 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 77228fd72a0..38e29b215ac 100644
--- a/packages/runtime-vapor/src/vdomInterop.ts
+++ b/packages/runtime-vapor/src/vdomInterop.ts
@@ -8,14 +8,17 @@ import {
type ShallowRef,
type Slots,
type VNode,
+ type VNodeNormalizedRef,
type VaporInteropInterface,
createVNode,
currentInstance,
ensureRenderer,
+ normalizeRef,
onScopeDispose,
renderSlot,
shallowRef,
simpleSetCurrentInstance,
+ setRef as vdomSetRef,
} from '@vue/runtime-dom'
import {
type LooseRawProps,
@@ -27,12 +30,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'
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<
@@ -45,7 +49,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)
// @ts-expect-error
@@ -169,9 +181,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)
}
@@ -186,6 +201,8 @@ function createVDOMComponent(
undefined,
false,
)
+ // set ref
+ if (rawRef) vdomSetRef(rawRef, null, null, vnode)
onScopeDispose(unmount, true)
isMounted = true
} else {
@@ -202,6 +219,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
}