]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-core): explicit expose API
authorEvan You <yyx990803@gmail.com>
Sat, 14 Nov 2020 17:49:35 +0000 (12:49 -0500)
committerEvan You <yyx990803@gmail.com>
Sat, 14 Nov 2020 17:49:35 +0000 (12:49 -0500)
packages/runtime-core/__tests__/apiExpose.spec.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/renderer.ts

diff --git a/packages/runtime-core/__tests__/apiExpose.spec.ts b/packages/runtime-core/__tests__/apiExpose.spec.ts
new file mode 100644 (file)
index 0000000..febf345
--- /dev/null
@@ -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()
+  })
+})
index 8f089eb4ee37a16610295e47375e7835e69a2e9c..af4c6b2f813993a112e30bbd3d7fa3c99bf6e4f4 100644 (file)
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
 export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
   extends ComponentInternalOptions {
   // use of any here is intentional so it can be a valid JSX Element constructor
-  (props: P, ctx: SetupContext<E>): any
+  (props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
   props?: ComponentPropsOptions<P>
   emits?: E | (keyof E)[]
   inheritAttrs?: boolean
@@ -171,6 +171,7 @@ export interface SetupContext<E = EmitsOptions> {
   attrs: Data
   slots: Slots
   emit: EmitFn<E>
+  expose: (exposed: Record<string, any>) => 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<string, any> | 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<Data> = {
 }
 
 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
     }
   }
 }
index ee4062ba8b96a7cd8ccb5abd288e215deb5552cd..e3dacf7b200a9875acc33f6bdfc706f4cf6aaa08 100644 (file)
@@ -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<string, Directive>
   inheritAttrs?: boolean
   emits?: (E | EE[]) & ThisType<void>
+  // TODO infer public instance type based on exposed keys
+  expose?: string[]
   serverPrefetch?(): Promise<any>
 
   // 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(
index f0182c16f6405e8ab14dc3b4c603a4b101e6ee84..9c3b6eefe5a76b8a6f612c304e2da67faaa51a83 100644 (file)
@@ -306,12 +306,12 @@ export const setRef = (
     return
   }
 
-  let value: ComponentPublicInstance | RendererNode | null
+  let value: ComponentPublicInstance | RendererNode | Record<string, any> | 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
     }