"
const _setTemplateRef = _createTemplateRefSetter()
const n0 = t0()
- _setTemplateRef(n0, foo)
+ _setTemplateRef(n0, foo, null, null, "foo")
return n0
"
`;
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', () => {
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,
),
]
}
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)]
}
// Built-in components
export { Teleport, type TeleportProps } from './components/Teleport'
export { Suspense, type SuspenseProps } from './components/Suspense'
-export { KeepAlive, type KeepAliveProps } from './components/KeepAlive'
+export {
+ KeepAlive,
+ type KeepAliveProps,
+ type KeepAliveContext,
+} from './components/KeepAlive'
export {
BaseTransition,
BaseTransitionPropsValidators,
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { setRef } from './rendererTemplateRef'
+/**
+ * @internal
+ */
+export { type VNodeNormalizedRef, normalizeRef } from './vnode'
/**
* @internal
*/
setupState: Data,
): (key: string) => boolean {
const rawSetupState = toRaw(setupState)
- return setupState === EMPTY_OBJ
+ return setupState === undefined || setupState === EMPTY_OBJ
? NO
: (key: string) => {
if (__DEV__) {
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
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
return define
}
+export { runtimeDom, runtimeVapor, VueServerRenderer }
+export function compile(
+ sfc: string,
+ data: runtimeDom.Ref<any>,
+ components: Record<string, any> = {},
+ {
+ vapor = true,
+ ssr = false,
+ }: {
+ vapor?: boolean | undefined
+ ssr?: boolean | undefined
+ } = {},
+): any {
+ if (!sfc.includes(`<script`)) {
+ sfc =
+ `<script vapor>const data = _data; const components = _components;</script>` +
+ 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,
+ )
+}
+
export function shuffle(array: Array<any>): any[] {
let currentIndex = array.length
let temporaryValue
createSlot,
createTemplateRefSetter,
defineVaporComponent,
+ delegateEvents,
insert,
renderEffect,
template,
} from '../../src'
-import { makeRender } from '../_utils'
+import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils'
import {
type ShallowRef,
currentInstance,
// 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<string, { code: string; vapor: boolean }> = {},
+ 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(
+ `<script setup>
+ import { useTemplateRef } from 'vue'
+ const components = _components;
+ const elRef = useTemplateRef('el')
+ function click() {
+ elRef.value.change()
+ }
+ </script>
+ <template>
+ <button class="btn" @click="click"></button>
+ <components.VaporChild ref="el"/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `
+ <script vapor>
+ import { ref } from 'vue'
+ const msg = ref('foo')
+ function change(){
+ msg.value = 'bar'
+ }
+ defineExpose({ change })
+ </script>
+ <template><div>{{msg}}</div></template>
+ `,
+ vapor: true,
+ },
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>foo</div>`,
+ )
+
+ const btn = container.querySelector('.btn')
+ triggerEvent('click', btn!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>bar</div>`,
+ )
+ })
+
+ test('vdom app: static ref with vapor child', async () => {
+ const { container } = await testTemplateRefInterop(
+ `<script setup>
+ import { ref } from 'vue'
+ const components = _components;
+ const elRef = ref(null)
+ function click() {
+ elRef.value.change()
+ }
+ </script>
+ <template>
+ <button class="btn" @click="click"></button>
+ <components.VaporChild ref="elRef"/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `
+ <script vapor>
+ import { ref } from 'vue'
+ const msg = ref('foo')
+ function change(){
+ msg.value = 'bar'
+ }
+ defineExpose({ change })
+ </script>
+ <template><div>{{msg}}</div></template>
+ `,
+ vapor: true,
+ },
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>foo</div>`,
+ )
+
+ const btn = container.querySelector('.btn')
+ triggerEvent('click', btn!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>bar</div>`,
+ )
+ })
+
+ test('vapor app: useTemplateRef with vdom child', async () => {
+ const { container } = await testTemplateRefInterop(
+ `<script vapor>
+ import { useTemplateRef } from 'vue'
+ const components = _components;
+ const elRef = useTemplateRef('el')
+ function click() {
+ elRef.value.change()
+ }
+ </script>
+ <template>
+ <button class="btn" @click="click"></button>
+ <components.VDOMChild ref="el"/>
+ </template>`,
+ {
+ VDOMChild: {
+ code: `
+ <script setup>
+ import { ref } from 'vue'
+ const msg = ref('foo')
+ function change(){
+ msg.value = 'bar'
+ }
+ defineExpose({ change })
+ </script>
+ <template><div>{{msg}}</div></template>
+ `,
+ vapor: false,
+ },
+ },
+ undefined,
+ { vapor: true },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>foo</div>`,
+ )
+
+ const btn = container.querySelector('.btn')
+ triggerEvent('click', btn!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>bar</div>`,
+ )
+ })
+
+ test('vapor app: static ref with vdom child', async () => {
+ const { container } = await testTemplateRefInterop(
+ `<script vapor>
+ import { ref } from 'vue'
+ const components = _components;
+ const elRef = ref(null)
+ function click() {
+ elRef.value.change()
+ }
+ </script>
+ <template>
+ <button class="btn" @click="click"></button>
+ <components.VDomChild ref="elRef"/>
+ </template>`,
+ {
+ VDomChild: {
+ code: `
+ <script setup>
+ import { ref } from 'vue'
+ const msg = ref('foo')
+ function change(){
+ msg.value = 'bar'
+ }
+ defineExpose({ change })
+ </script>
+ <template><div>{{msg}}</div></template>
+ `,
+ vapor: false,
+ },
+ },
+ undefined,
+ { vapor: true },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>foo</div>`,
+ )
+
+ const btn = container.querySelector('.btn')
+ triggerEvent('click', btn!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button class="btn"></button><div>bar</div>`,
+ )
+ })
+})
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<any>,
- components: Record<string, any> = {},
- ssr = false,
-) {
- if (!sfc.includes(`<script`)) {
- sfc =
- `<script vapor>const data = _data; const components = _components;</script>` +
- 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,
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),
)
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<string, any>) => void)
export type RefEl = Element | VaporComponentInstance
export type setRefFn = (
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)
const refs =
}
} else {
ref.value = existing
+ if (refKey) refs[refKey] = existing
}
} else if (!existing.includes(refValue)) {
existing.push(refValue)
}
} else if (_isRef) {
ref.value = refValue
+ if (refKey) refs[refKey] = refValue
} else if (__DEV__) {
warn('Invalid template ref type:', ref, `(${typeof ref})`)
}
}
} else if (_isRef) {
ref.value = null
+ if (refKey) refs[refKey] = null
}
})
})
import { createComment, createTextNode } from './dom/node'
import { EffectScope, setActiveSub } from '@vue/reactivity'
import { isHydrating } from './dom/hydration'
+import type { NodeRef } from './apiTemplateRef'
import {
type TransitionHooks,
type TransitionProps,
nodes: T
vnode?: VNode | null = null
anchor?: Node
+ setRef?: (
+ instance: VaporComponentInstance,
+ ref: NodeRef,
+ refFor: boolean,
+ refKey: string | undefined,
+ ) => void
fallback?: BlockFn
$key?: any
$transition?: VaporTransitionHooks | undefined
type App,
type ComponentInternalInstance,
type ConcreteComponent,
+ type KeepAliveContext,
MoveType,
type Plugin,
type RendererElement,
type Slots,
type TransitionHooks,
type VNode,
+ type VNodeNormalizedRef,
type VaporInteropInterface,
createInternalObject,
createVNode,
isEmitListener,
isKeepAlive,
isVNode,
+ normalizeRef,
onScopeDispose,
renderSlot,
setTransitionHooks as setVNodeTransitionHooks,
simpleSetCurrentInstance,
activate as vdomActivate,
deactivate as vdomDeactivate,
+ setRef as vdomSetRef,
} from '@vue/runtime-dom'
import {
type LooseRawProps,
import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
+import type { NodeRef } from './apiTemplateRef'
import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
import {
type KeepAliveInstance,
activate,
deactivate,
} from './components/KeepAlive'
-import type { KeepAliveContext } from 'packages/runtime-core/src/components/KeepAlive'
export const interopKey: unique symbol = Symbol(`interop`)
: new Proxy(wrapper.slots, vaporSlotsProxyHandler)
}
+ let rawRef: VNodeNormalizedRef | null = null
let isMounted = false
const parentInstance = currentInstance as VaporComponentInstance
const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
+ // unset ref
+ if (rawRef) vdomSetRef(rawRef, null, null, vnode, true)
if (transition) setVNodeTransitionHooks(vnode, transition)
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vdomDeactivate(
undefined,
false,
)
+ // set ref
+ if (rawRef) vdomSetRef(rawRef, null, null, vnode)
onScopeDispose(unmount, true)
isMounted = true
} else {
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
}