From 0e59770b9282992f6a5af4d8fef33dafb948fc8b Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 14 Nov 2020 12:49:35 -0500 Subject: [PATCH] feat(runtime-core): explicit expose API --- .../runtime-core/__tests__/apiExpose.spec.ts | 98 +++++++++++++++++++ packages/runtime-core/src/component.ts | 20 +++- packages/runtime-core/src/componentOptions.ts | 17 +++- packages/runtime-core/src/renderer.ts | 4 +- 4 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 packages/runtime-core/__tests__/apiExpose.spec.ts 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 }