feat(runtime-core): explicit expose API
This commit is contained in:
		
							parent
							
								
									15baaf14f0
								
							
						
					
					
						commit
						0e59770b92
					
				
							
								
								
									
										98
									
								
								packages/runtime-core/__tests__/apiExpose.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								packages/runtime-core/__tests__/apiExpose.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @ -105,7 +105,7 @@ export interface ComponentInternalOptions { | |||||||
| export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> | export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> | ||||||
|   extends ComponentInternalOptions { |   extends ComponentInternalOptions { | ||||||
|   // use of any here is intentional so it can be a valid JSX Element constructor
 |   // 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> |   props?: ComponentPropsOptions<P> | ||||||
|   emits?: E | (keyof E)[] |   emits?: E | (keyof E)[] | ||||||
|   inheritAttrs?: boolean |   inheritAttrs?: boolean | ||||||
| @ -171,6 +171,7 @@ export interface SetupContext<E = EmitsOptions> { | |||||||
|   attrs: Data |   attrs: Data | ||||||
|   slots: Slots |   slots: Slots | ||||||
|   emit: EmitFn<E> |   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`)
 |   // main proxy that serves as the public instance (`this`)
 | ||||||
|   proxy: ComponentPublicInstance | null |   proxy: ComponentPublicInstance | null | ||||||
| 
 | 
 | ||||||
|  |   // exposed properties via expose()
 | ||||||
|  |   exposed: Record<string, any> | null | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * alternative proxy used only for runtime-compiled render functions using |    * alternative proxy used only for runtime-compiled render functions using | ||||||
|    * `with` block |    * `with` block | ||||||
| @ -415,6 +419,7 @@ export function createComponentInstance( | |||||||
|     update: null!, // will be set synchronously right after creation
 |     update: null!, // will be set synchronously right after creation
 | ||||||
|     render: null, |     render: null, | ||||||
|     proxy: null, |     proxy: null, | ||||||
|  |     exposed: null, | ||||||
|     withProxy: null, |     withProxy: null, | ||||||
|     effects: null, |     effects: null, | ||||||
|     provides: parent ? parent.provides : Object.create(appContext.provides), |     provides: parent ? parent.provides : Object.create(appContext.provides), | ||||||
| @ -731,6 +736,13 @@ const attrHandlers: ProxyHandler<Data> = { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function createSetupContext(instance: ComponentInternalInstance): SetupContext { | 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__) { |   if (__DEV__) { | ||||||
|     // We use getters in dev in case libs like test-utils overwrite instance
 |     // We use getters in dev in case libs like test-utils overwrite instance
 | ||||||
|     // properties (overwrites should not be done in prod)
 |     // properties (overwrites should not be done in prod)
 | ||||||
| @ -743,13 +755,15 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext { | |||||||
|       }, |       }, | ||||||
|       get emit() { |       get emit() { | ||||||
|         return (event: string, ...args: any[]) => instance.emit(event, ...args) |         return (event: string, ...args: any[]) => instance.emit(event, ...args) | ||||||
|       } |       }, | ||||||
|  |       expose | ||||||
|     }) |     }) | ||||||
|   } else { |   } else { | ||||||
|     return { |     return { | ||||||
|       attrs: instance.attrs, |       attrs: instance.attrs, | ||||||
|       slots: instance.slots, |       slots: instance.slots, | ||||||
|       emit: instance.emit |       emit: instance.emit, | ||||||
|  |       expose | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -41,7 +41,9 @@ import { | |||||||
|   reactive, |   reactive, | ||||||
|   ComputedGetter, |   ComputedGetter, | ||||||
|   WritableComputedOptions, |   WritableComputedOptions, | ||||||
|   toRaw |   toRaw, | ||||||
|  |   proxyRefs, | ||||||
|  |   toRef | ||||||
| } from '@vue/reactivity' | } from '@vue/reactivity' | ||||||
| import { | import { | ||||||
|   ComponentObjectPropsOptions, |   ComponentObjectPropsOptions, | ||||||
| @ -110,6 +112,8 @@ export interface ComponentOptionsBase< | |||||||
|   directives?: Record<string, Directive> |   directives?: Record<string, Directive> | ||||||
|   inheritAttrs?: boolean |   inheritAttrs?: boolean | ||||||
|   emits?: (E | EE[]) & ThisType<void> |   emits?: (E | EE[]) & ThisType<void> | ||||||
|  |   // TODO infer public instance type based on exposed keys
 | ||||||
|  |   expose?: string[] | ||||||
|   serverPrefetch?(): Promise<any> |   serverPrefetch?(): Promise<any> | ||||||
| 
 | 
 | ||||||
|   // Internal ------------------------------------------------------------------
 |   // Internal ------------------------------------------------------------------
 | ||||||
| @ -461,7 +465,9 @@ export function applyOptions( | |||||||
|     render, |     render, | ||||||
|     renderTracked, |     renderTracked, | ||||||
|     renderTriggered, |     renderTriggered, | ||||||
|     errorCaptured |     errorCaptured, | ||||||
|  |     // public API
 | ||||||
|  |     expose | ||||||
|   } = options |   } = options | ||||||
| 
 | 
 | ||||||
|   const publicThis = instance.proxy! |   const publicThis = instance.proxy! | ||||||
| @ -736,6 +742,13 @@ export function applyOptions( | |||||||
|   if (unmounted) { |   if (unmounted) { | ||||||
|     onUnmounted(unmounted.bind(publicThis)) |     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( | function callSyncHook( | ||||||
|  | |||||||
| @ -306,12 +306,12 @@ export const setRef = ( | |||||||
|     return |     return | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let value: ComponentPublicInstance | RendererNode | null |   let value: ComponentPublicInstance | RendererNode | Record<string, any> | null | ||||||
|   if (!vnode) { |   if (!vnode) { | ||||||
|     value = null |     value = null | ||||||
|   } else { |   } else { | ||||||
|     if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { |     if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { | ||||||
|       value = vnode.component!.proxy |       value = vnode.component!.exposed || vnode.component!.proxy | ||||||
|     } else { |     } else { | ||||||
|       value = vnode.el |       value = vnode.el | ||||||
|     } |     } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user