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 = {}>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user