diff --git a/packages/runtime-core/__tests__/apiExpose.spec.ts b/packages/runtime-core/__tests__/apiExpose.spec.ts
new file mode 100644
index 00000000000..febf3452409
--- /dev/null
+++ b/packages/runtime-core/__tests__/apiExpose.spec.ts
@@ -0,0 +1,98 @@
+import { nodeOps, render } from '@vue/runtime-test'
+import { defineComponent, h, ref } from '../src'
+
+describe('api: expose', () => {
+ test('via setup context', () => {
+ const Child = defineComponent({
+ render() {},
+ setup(_, { expose }) {
+ expose({
+ foo: ref(1),
+ bar: ref(2)
+ })
+ return {
+ bar: ref(3),
+ baz: ref(4)
+ }
+ }
+ })
+
+ const childRef = ref()
+ const Parent = {
+ setup() {
+ return () => h(Child, { ref: childRef })
+ }
+ }
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(childRef.value).toBeTruthy()
+ expect(childRef.value.foo).toBe(1)
+ expect(childRef.value.bar).toBe(2)
+ expect(childRef.value.baz).toBeUndefined()
+ })
+
+ test('via options', () => {
+ const Child = defineComponent({
+ render() {},
+ data() {
+ return {
+ foo: 1
+ }
+ },
+ setup() {
+ return {
+ bar: ref(2),
+ baz: ref(3)
+ }
+ },
+ expose: ['foo', 'bar']
+ })
+
+ const childRef = ref()
+ const Parent = {
+ setup() {
+ return () => h(Child, { ref: childRef })
+ }
+ }
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(childRef.value).toBeTruthy()
+ expect(childRef.value.foo).toBe(1)
+ expect(childRef.value.bar).toBe(2)
+ expect(childRef.value.baz).toBeUndefined()
+ })
+
+ test('options + context', () => {
+ const Child = defineComponent({
+ render() {},
+ expose: ['foo'],
+ data() {
+ return {
+ foo: 1
+ }
+ },
+ setup(_, { expose }) {
+ expose({
+ bar: ref(2)
+ })
+ return {
+ bar: ref(3),
+ baz: ref(4)
+ }
+ }
+ })
+
+ const childRef = ref()
+ const Parent = {
+ setup() {
+ return () => h(Child, { ref: childRef })
+ }
+ }
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(childRef.value).toBeTruthy()
+ expect(childRef.value.foo).toBe(1)
+ expect(childRef.value.bar).toBe(2)
+ expect(childRef.value.baz).toBeUndefined()
+ })
+})
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 8f089eb4ee3..af4c6b2f813 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
export interface FunctionalComponent
extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor
- (props: P, ctx: SetupContext): any
+ (props: P, ctx: Omit, 'expose'>): any
props?: ComponentPropsOptions
emits?: E | (keyof E)[]
inheritAttrs?: boolean
@@ -171,6 +171,7 @@ export interface SetupContext {
attrs: Data
slots: Slots
emit: EmitFn
+ expose: (exposed: Record) => void
}
/**
@@ -270,6 +271,9 @@ export interface ComponentInternalInstance {
// main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null
+ // exposed properties via expose()
+ exposed: Record | null
+
/**
* alternative proxy used only for runtime-compiled render functions using
* `with` block
@@ -415,6 +419,7 @@ export function createComponentInstance(
update: null!, // will be set synchronously right after creation
render: null,
proxy: null,
+ exposed: null,
withProxy: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
@@ -731,6 +736,13 @@ const attrHandlers: ProxyHandler = {
}
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
+ const expose: SetupContext['expose'] = exposed => {
+ if (__DEV__ && instance.exposed) {
+ warn(`expose() should be called only once per setup().`)
+ }
+ instance.exposed = proxyRefs(exposed)
+ }
+
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
@@ -743,13 +755,15 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
- }
+ },
+ expose
})
} else {
return {
attrs: instance.attrs,
slots: instance.slots,
- emit: instance.emit
+ emit: instance.emit,
+ expose
}
}
}
diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts
index ee4062ba8b9..e3dacf7b200 100644
--- a/packages/runtime-core/src/componentOptions.ts
+++ b/packages/runtime-core/src/componentOptions.ts
@@ -41,7 +41,9 @@ import {
reactive,
ComputedGetter,
WritableComputedOptions,
- toRaw
+ toRaw,
+ proxyRefs,
+ toRef
} from '@vue/reactivity'
import {
ComponentObjectPropsOptions,
@@ -110,6 +112,8 @@ export interface ComponentOptionsBase<
directives?: Record
inheritAttrs?: boolean
emits?: (E | EE[]) & ThisType
+ // TODO infer public instance type based on exposed keys
+ expose?: string[]
serverPrefetch?(): Promise
// Internal ------------------------------------------------------------------
@@ -461,7 +465,9 @@ export function applyOptions(
render,
renderTracked,
renderTriggered,
- errorCaptured
+ errorCaptured,
+ // public API
+ expose
} = options
const publicThis = instance.proxy!
@@ -736,6 +742,13 @@ export function applyOptions(
if (unmounted) {
onUnmounted(unmounted.bind(publicThis))
}
+
+ if (!asMixin && expose) {
+ const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
+ expose.forEach(key => {
+ exposed[key] = toRef(publicThis, key as any)
+ })
+ }
}
function callSyncHook(
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index f0182c16f64..9c3b6eefe5a 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -306,12 +306,12 @@ export const setRef = (
return
}
- let value: ComponentPublicInstance | RendererNode | null
+ let value: ComponentPublicInstance | RendererNode | Record | null
if (!vnode) {
value = null
} else {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
- value = vnode.component!.proxy
+ value = vnode.component!.exposed || vnode.component!.proxy
} else {
value = vnode.el
}