diff --git a/packages/runtime-vapor/__tests__/apiExpose.spec.ts b/packages/runtime-vapor/__tests__/apiExpose.spec.ts new file mode 100644 index 000000000..4323991d0 --- /dev/null +++ b/packages/runtime-vapor/__tests__/apiExpose.spec.ts @@ -0,0 +1,97 @@ +import { ref, shallowRef } from '@vue/reactivity' +import { createComponent } from '../src/apiCreateComponent' +import { setRef } from '../src/dom/templateRef' +import { makeRender } from './_utils' +import { + type ComponentInternalInstance, + getCurrentInstance, +} from '../src/component' + +const define = makeRender() +describe('api: expose', () => { + test('via setup context', () => { + const { component: Child } = define({ + setup(_, { expose }) { + expose({ + foo: 1, + bar: ref(2), + }) + return { + bar: ref(3), + baz: ref(4), + } + }, + }) + const childRef = ref() + const { render } = define({ + render: () => { + const n0 = createComponent(Child) + setRef(n0, childRef) + return n0 + }, + }) + + render() + expect(childRef.value).toBeTruthy() + expect(childRef.value.foo).toBe(1) + expect(childRef.value.bar).toBe(2) + expect(childRef.value.baz).toBeUndefined() + }) + + test('via setup context (expose empty)', () => { + let childInstance: ComponentInternalInstance | null = null + const { component: Child } = define({ + setup(_) { + childInstance = getCurrentInstance() + }, + }) + const childRef = shallowRef() + const { render } = define({ + render: () => { + const n0 = createComponent(Child) + setRef(n0, childRef) + return n0 + }, + }) + + render() + expect(childInstance!.exposed).toBeUndefined() + expect(childRef.value).toBe(childInstance!) + }) + + test('warning for ref', () => { + const { render } = define({ + setup(_, { expose }) { + expose(ref(1)) + }, + }) + render() + expect( + 'expose() should be passed a plain object, received ref', + ).toHaveBeenWarned() + }) + + test('warning for array', () => { + const { render } = define({ + setup(_, { expose }) { + expose(['focus']) + }, + }) + render() + expect( + 'expose() should be passed a plain object, received array', + ).toHaveBeenWarned() + }) + + test('warning for function', () => { + const { render } = define({ + setup(_, { expose }) { + expose(() => null) + }, + }) + render() + expect( + 'expose() should be passed a plain object, received function', + ).toHaveBeenWarned() + }) +}) diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts new file mode 100644 index 000000000..6dc10eff3 --- /dev/null +++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts @@ -0,0 +1,26 @@ +import { ref, setRef, template } from '../../src' +import { makeRender } from '../_utils' + +const define = makeRender() + +describe('api: template ref', () => { + test('string ref mount', () => { + const t0 = template('
') + const el = ref(null) + const { render } = define({ + setup() { + return { + refKey: el, + } + }, + render() { + const n0 = t0() + setRef(n0 as Element, 'refKey') + return n0 + }, + }) + + const { host } = render() + expect(el.value).toBe(host.children[0]) + }) +}) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 43e89aaa5..4725a14b6 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,5 +1,5 @@ import { EffectScope, isRef } from '@vue/reactivity' -import { EMPTY_OBJ, isArray, isFunction } from '@vue/shared' +import { EMPTY_OBJ, hasOwn, isArray, isFunction } from '@vue/shared' import type { Block } from './apiRender' import type { DirectiveBinding } from './directives' import { @@ -327,6 +327,12 @@ export function createComponentInstance( return instance } +export function isVaporComponent( + val: unknown, +): val is ComponentInternalInstance { + return !!val && hasOwn(val, componentKey) +} + function getAttrsProxy(instance: ComponentInternalInstance): Data { return ( instance.attrsProxy || diff --git a/packages/runtime-vapor/src/dom/templateRef.ts b/packages/runtime-vapor/src/dom/templateRef.ts index 2faeee53e..25a9b014b 100644 --- a/packages/runtime-vapor/src/dom/templateRef.ts +++ b/packages/runtime-vapor/src/dom/templateRef.ts @@ -4,7 +4,11 @@ import { isRef, onScopeDispose, } from '@vue/reactivity' -import { currentInstance } from '../component' +import { + type ComponentInternalInstance, + currentInstance, + isVaporComponent, +} from '../component' import { VaporErrorCodes, callWithErrorHandling } from '../errorHandling' import { EMPTY_OBJ, @@ -18,11 +22,12 @@ import { warn } from '../warning' import { queuePostFlushCb } from '../scheduler' export type NodeRef = string | Ref | ((ref: Element) => void) +export type RefEl = Element | ComponentInternalInstance /** * Function for handling a template ref */ -export function setRef(el: Element, ref: NodeRef, refFor = false) { +export function setRef(el: RefEl, ref: NodeRef, refFor = false) { if (!currentInstance) return const { setupState, isUnmounted } = currentInstance @@ -30,13 +35,15 @@ export function setRef(el: Element, ref: NodeRef, refFor = false) { return } + const refValue = isVaporComponent(el) ? el.exposed || el : el + const refs = currentInstance.refs === EMPTY_OBJ ? (currentInstance.refs = {}) : currentInstance.refs if (isFunction(ref)) { - const invokeRefSetter = (value: Element | null) => { + const invokeRefSetter = (value?: Element | Record