Skip to content

Commit 0e59770

Browse files
committed
feat(runtime-core): explicit expose API
1 parent 15baaf1 commit 0e59770

File tree

4 files changed

+132
-7
lines changed

4 files changed

+132
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { nodeOps, render } from '@vue/runtime-test'
2+
import { defineComponent, h, ref } from '../src'
3+
4+
describe('api: expose', () => {
5+
test('via setup context', () => {
6+
const Child = defineComponent({
7+
render() {},
8+
setup(_, { expose }) {
9+
expose({
10+
foo: ref(1),
11+
bar: ref(2)
12+
})
13+
return {
14+
bar: ref(3),
15+
baz: ref(4)
16+
}
17+
}
18+
})
19+
20+
const childRef = ref()
21+
const Parent = {
22+
setup() {
23+
return () => h(Child, { ref: childRef })
24+
}
25+
}
26+
const root = nodeOps.createElement('div')
27+
render(h(Parent), root)
28+
expect(childRef.value).toBeTruthy()
29+
expect(childRef.value.foo).toBe(1)
30+
expect(childRef.value.bar).toBe(2)
31+
expect(childRef.value.baz).toBeUndefined()
32+
})
33+
34+
test('via options', () => {
35+
const Child = defineComponent({
36+
render() {},
37+
data() {
38+
return {
39+
foo: 1
40+
}
41+
},
42+
setup() {
43+
return {
44+
bar: ref(2),
45+
baz: ref(3)
46+
}
47+
},
48+
expose: ['foo', 'bar']
49+
})
50+
51+
const childRef = ref()
52+
const Parent = {
53+
setup() {
54+
return () => h(Child, { ref: childRef })
55+
}
56+
}
57+
const root = nodeOps.createElement('div')
58+
render(h(Parent), root)
59+
expect(childRef.value).toBeTruthy()
60+
expect(childRef.value.foo).toBe(1)
61+
expect(childRef.value.bar).toBe(2)
62+
expect(childRef.value.baz).toBeUndefined()
63+
})
64+
65+
test('options + context', () => {
66+
const Child = defineComponent({
67+
render() {},
68+
expose: ['foo'],
69+
data() {
70+
return {
71+
foo: 1
72+
}
73+
},
74+
setup(_, { expose }) {
75+
expose({
76+
bar: ref(2)
77+
})
78+
return {
79+
bar: ref(3),
80+
baz: ref(4)
81+
}
82+
}
83+
})
84+
85+
const childRef = ref()
86+
const Parent = {
87+
setup() {
88+
return () => h(Child, { ref: childRef })
89+
}
90+
}
91+
const root = nodeOps.createElement('div')
92+
render(h(Parent), root)
93+
expect(childRef.value).toBeTruthy()
94+
expect(childRef.value.foo).toBe(1)
95+
expect(childRef.value.bar).toBe(2)
96+
expect(childRef.value.baz).toBeUndefined()
97+
})
98+
})

packages/runtime-core/src/component.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
105105
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
106106
extends ComponentInternalOptions {
107107
// use of any here is intentional so it can be a valid JSX Element constructor
108-
(props: P, ctx: SetupContext<E>): any
108+
(props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
109109
props?: ComponentPropsOptions<P>
110110
emits?: E | (keyof E)[]
111111
inheritAttrs?: boolean
@@ -171,6 +171,7 @@ export interface SetupContext<E = EmitsOptions> {
171171
attrs: Data
172172
slots: Slots
173173
emit: EmitFn<E>
174+
expose: (exposed: Record<string, any>) => void
174175
}
175176

176177
/**
@@ -270,6 +271,9 @@ export interface ComponentInternalInstance {
270271
// main proxy that serves as the public instance (`this`)
271272
proxy: ComponentPublicInstance | null
272273

274+
// exposed properties via expose()
275+
exposed: Record<string, any> | null
276+
273277
/**
274278
* alternative proxy used only for runtime-compiled render functions using
275279
* `with` block
@@ -415,6 +419,7 @@ export function createComponentInstance(
415419
update: null!, // will be set synchronously right after creation
416420
render: null,
417421
proxy: null,
422+
exposed: null,
418423
withProxy: null,
419424
effects: null,
420425
provides: parent ? parent.provides : Object.create(appContext.provides),
@@ -731,6 +736,13 @@ const attrHandlers: ProxyHandler<Data> = {
731736
}
732737

733738
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
739+
const expose: SetupContext['expose'] = exposed => {
740+
if (__DEV__ && instance.exposed) {
741+
warn(`expose() should be called only once per setup().`)
742+
}
743+
instance.exposed = proxyRefs(exposed)
744+
}
745+
734746
if (__DEV__) {
735747
// We use getters in dev in case libs like test-utils overwrite instance
736748
// properties (overwrites should not be done in prod)
@@ -743,13 +755,15 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
743755
},
744756
get emit() {
745757
return (event: string, ...args: any[]) => instance.emit(event, ...args)
746-
}
758+
},
759+
expose
747760
})
748761
} else {
749762
return {
750763
attrs: instance.attrs,
751764
slots: instance.slots,
752-
emit: instance.emit
765+
emit: instance.emit,
766+
expose
753767
}
754768
}
755769
}

packages/runtime-core/src/componentOptions.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ import {
4141
reactive,
4242
ComputedGetter,
4343
WritableComputedOptions,
44-
toRaw
44+
toRaw,
45+
proxyRefs,
46+
toRef
4547
} from '@vue/reactivity'
4648
import {
4749
ComponentObjectPropsOptions,
@@ -110,6 +112,8 @@ export interface ComponentOptionsBase<
110112
directives?: Record<string, Directive>
111113
inheritAttrs?: boolean
112114
emits?: (E | EE[]) & ThisType<void>
115+
// TODO infer public instance type based on exposed keys
116+
expose?: string[]
113117
serverPrefetch?(): Promise<any>
114118

115119
// Internal ------------------------------------------------------------------
@@ -461,7 +465,9 @@ export function applyOptions(
461465
render,
462466
renderTracked,
463467
renderTriggered,
464-
errorCaptured
468+
errorCaptured,
469+
// public API
470+
expose
465471
} = options
466472

467473
const publicThis = instance.proxy!
@@ -736,6 +742,13 @@ export function applyOptions(
736742
if (unmounted) {
737743
onUnmounted(unmounted.bind(publicThis))
738744
}
745+
746+
if (!asMixin && expose) {
747+
const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
748+
expose.forEach(key => {
749+
exposed[key] = toRef(publicThis, key as any)
750+
})
751+
}
739752
}
740753

741754
function callSyncHook(

packages/runtime-core/src/renderer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,12 @@ export const setRef = (
306306
return
307307
}
308308

309-
let value: ComponentPublicInstance | RendererNode | null
309+
let value: ComponentPublicInstance | RendererNode | Record<string, any> | null
310310
if (!vnode) {
311311
value = null
312312
} else {
313313
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
314-
value = vnode.component!.proxy
314+
value = vnode.component!.exposed || vnode.component!.proxy
315315
} else {
316316
value = vnode.el
317317
}

0 commit comments

Comments
 (0)