Skip to content

Commit

Permalink
feat: implement api useSlots and useAttrs (#800)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Aug 21, 2021
1 parent 72a878d commit 1e6e3a9
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 54 deletions.
1 change: 1 addition & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export {
getCurrentScope,
onScopeDispose,
} from './effectScope'
export { useAttrs, useSlots } from './setupHelpers'
18 changes: 18 additions & 0 deletions src/apis/setupHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getCurrentInstance, SetupContext } from '../runtimeContext'
import { warn } from '../utils'

export function useSlots(): SetupContext['slots'] {
return getContext().slots
}

export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
}

function getContext(): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useContext() called without active instance.`)
}
return i.setupContext!
}
29 changes: 2 additions & 27 deletions src/component/componentOptions.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,9 @@
import Vue, { VNode, ComponentOptions as Vue2ComponentOptions } from 'vue'
import { SetupContext } from '../runtimeContext'
import { Data } from './common'
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { ComponentInstance, ComponentRenderProxy } from './componentProxy'
import { ComponentRenderProxy } from './componentProxy'
export { ComponentPropsOptions } from './componentProps'
export interface SetupContext {
readonly attrs: Data
readonly slots: Readonly<{ [key in string]?: (...args: any[]) => VNode[] }>

/**
* @deprecated not available in Vue 3
*/
readonly parent: ComponentInstance | null

/**
* @deprecated not available in Vue 3
*/
readonly root: ComponentInstance

/**
* @deprecated not available in Vue 3
*/
readonly listeners: { [key in string]?: Function }

/**
* @deprecated not available in Vue 3
*/
readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }

emit(event: string, ...args: any[]): void
}

export type ComputedGetter<T> = (ctx?: any) => T
export type ComputedSetter<T> = (v: T) => void
Expand Down
1 change: 0 additions & 1 deletion src/component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export { defineComponent } from './defineComponent'
export { defineAsyncComponent } from './defineAsyncComponent'
export {
SetupFunction,
SetupContext,
ComputedOptions,
MethodOptions,
ComponentPropsOptions,
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export const version = __VERSION__

export * from './apis'
export * from './component'
export { getCurrentInstance, ComponentInternalInstance } from './runtimeContext'
export {
getCurrentInstance,
ComponentInternalInstance,
SetupContext,
} from './runtimeContext'

export default Plugin

Expand Down
33 changes: 18 additions & 15 deletions src/mixin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type { VueConstructor } from 'vue'
import {
ComponentInstance,
SetupContext,
SetupFunction,
Data,
} from './component'
import { ComponentInstance, SetupFunction, Data } from './component'
import { isRef, isReactive, toRefs, isRaw } from './reactivity'
import {
isPlainObject,
Expand All @@ -24,7 +19,11 @@ import {
resolveScopedSlots,
asVmProperty,
} from './utils/instance'
import { getVueConstructor } from './runtimeContext'
import {
getVueConstructor,
SetupContext,
toVue3ComponentInstance,
} from './runtimeContext'
import { createObserver, reactive } from './reactivity/reactive'

export function mixin(Vue: VueConstructor) {
Expand Down Expand Up @@ -53,7 +52,9 @@ export function mixin(Vue: VueConstructor) {
if (render) {
// keep currentInstance accessible for createElement
$options.render = function (...args: any): any {
return activateCurrentInstance(vm, () => render.apply(this, args))
return activateCurrentInstance(toVue3ComponentInstance(vm), () =>
render.apply(this, args)
)
}
}

Expand Down Expand Up @@ -85,16 +86,17 @@ export function mixin(Vue: VueConstructor) {
function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) {
const setup = vm.$options.setup!
const ctx = createSetupContext(vm)
const instance = toVue3ComponentInstance(vm)
instance.setupContext = ctx

// fake reactive for `toRefs(props)`
def(props, '__ob__', createObserver())

// resolve scopedSlots and slots to functions
// @ts-expect-error
resolveScopedSlots(vm, ctx.slots)

let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
activateCurrentInstance(vm, () => {
activateCurrentInstance(instance, () => {
// make props to be fake reactive, this is for `toRefs(props)`
binding = setup(props, ctx)
})
Expand All @@ -105,9 +107,8 @@ export function mixin(Vue: VueConstructor) {
const bindingFunc = binding
// keep currentInstance accessible for createElement
vm.$options.render = () => {
// @ts-expect-error
resolveScopedSlots(vm, ctx.slots)
return activateCurrentInstance(vm, () => bindingFunc())
return activateCurrentInstance(instance, () => bindingFunc())
}
return
} else if (isPlainObject(binding)) {
Expand Down Expand Up @@ -228,23 +229,25 @@ export function mixin(Vue: VueConstructor) {
})
})

let propsProxy: any
propsReactiveProxy.forEach((key) => {
let srcKey = `$${key}`
proxy(ctx, key, {
get: () => {
const data = reactive({})
if (propsProxy) return propsProxy
propsProxy = reactive({})
const source = vm[srcKey]

for (const attr of Object.keys(source)) {
proxy(data, attr, {
proxy(propsProxy, attr, {
get: () => {
// to ensure it always return the latest value
return vm[srcKey][attr]
},
})
}

return data
return propsProxy
},
set() {
__DEV__ &&
Expand Down
39 changes: 38 additions & 1 deletion src/runtimeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,38 @@ export type EmitFn<
}[Event]
>

export type Slots = Readonly<InternalSlots>

export interface SetupContext<E = EmitsOptions> {
attrs: Data
slots: Slots
emit: EmitFn<E>
/**
* @deprecated not available in Vue 2
*/
expose: (exposed?: Record<string, any>) => void

/**
* @deprecated not available in Vue 3
*/
readonly parent: ComponentInstance | null

/**
* @deprecated not available in Vue 3
*/
readonly root: ComponentInstance

/**
* @deprecated not available in Vue 3
*/
readonly listeners: { [key in string]?: Function }

/**
* @deprecated not available in Vue 3
*/
readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }
}

/**
* We expose a subset of properties on the internal instance as they are
* useful for advanced external libraries and tools.
Expand Down Expand Up @@ -179,6 +211,11 @@ export declare interface ComponentInternalInstance {
* @internal
*/
scope: EffectScope

/**
* @internal
*/
setupContext: SetupContext | null
}

export function getCurrentInstance() {
Expand All @@ -190,7 +227,7 @@ const instanceMapCache = new WeakMap<
ComponentInternalInstance
>()

function toVue3ComponentInstance(
export function toVue3ComponentInstance(
vm: ComponentInstance
): ComponentInternalInstance {
if (instanceMapCache.has(vm)) {
Expand Down
7 changes: 4 additions & 3 deletions src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ComponentInternalInstance,
getCurrentInstance,
getVueConstructor,
Slot,
} from '../runtimeContext'
import { warn } from './utils'

Expand Down Expand Up @@ -38,8 +39,8 @@ export function isComponentInstance(obj: any) {
return Vue && obj instanceof Vue
}

export function createSlotProxy(vm: ComponentInstance, slotName: string) {
return (...args: any) => {
export function createSlotProxy(vm: ComponentInstance, slotName: string): Slot {
return ((...args: any) => {
if (!vm.$scopedSlots[slotName]) {
if (__DEV__)
return warn(
Expand All @@ -50,7 +51,7 @@ export function createSlotProxy(vm: ComponentInstance, slotName: string) {
}

return vm.$scopedSlots[slotName]!.apply(vm, args)
}
}) as Slot
}

export function resolveSlots(
Expand Down
13 changes: 7 additions & 6 deletions src/utils/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import vmStateManager from './vmStateManager'
import {
setCurrentInstance,
getCurrentInstance,
setCurrentVue2Instance,
ComponentInternalInstance,
InternalSlots,
} from '../runtimeContext'
import { Ref, isRef, isReactive } from '../apis'
import { hasOwn, proxy, warn } from './utils'
Expand Down Expand Up @@ -102,7 +103,7 @@ export function updateTemplateRef(vm: ComponentInstance) {

export function resolveScopedSlots(
vm: ComponentInstance,
slotsProxy: { [x: string]: Function }
slotsProxy: InternalSlots
): void {
const parentVNode = (vm.$options as any)._parentVnode
if (!parentVNode) return
Expand All @@ -129,14 +130,14 @@ export function resolveScopedSlots(
}

export function activateCurrentInstance(
vm: ComponentInstance,
fn: (vm_: ComponentInstance) => any,
instance: ComponentInternalInstance,
fn: (instance: ComponentInternalInstance) => any,
onError?: (err: Error) => void
) {
let preVm = getCurrentInstance()
setCurrentVue2Instance(vm)
setCurrentInstance(instance)
try {
return fn(vm)
return fn(instance)
} catch (err) {
if (onError) {
onError(err)
Expand Down
49 changes: 49 additions & 0 deletions test/v3/runtime-core/apiSetupHelpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
createApp,
defineComponent,
SetupContext,
useAttrs,
useSlots,
} from '../../../src'

describe('SFC <script setup> helpers', () => {
// test('useSlots / useAttrs (no args)', () => {
// let slots: SetupContext['slots'] | undefined
// let attrs: SetupContext['attrs'] | undefined
// const Comp = {
// setup() {
// slots = useSlots()
// attrs = useAttrs()
// return () => {}
// }
// }
// const passedAttrs = { id: 'foo' }
// const passedSlots = {
// default: () => {},
// x: () => {}
// }
// const root = document.createElement('div')
// const vm = createApp(Comp).mount(root)
// expect(typeof slots!.default).toBe('function')
// expect(typeof slots!.x).toBe('function')
// expect(attrs).toMatchObject(passedAttrs)
// })

test('useSlots / useAttrs (with args)', () => {
let slots: SetupContext['slots'] | undefined
let attrs: SetupContext['attrs'] | undefined
let ctx: SetupContext | undefined
const Comp = defineComponent({
setup(_, _ctx) {
slots = useSlots()
attrs = useAttrs()
ctx = _ctx
return () => {}
},
})
const root = document.createElement('div')
createApp(Comp, { foo: 'bar' }).mount(root)
expect(slots).toBe(ctx!.slots)
expect(attrs).toBe(ctx!.attrs)
})
})

0 comments on commit 1e6e3a9

Please sign in to comment.