Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement api useSlots and useAttrs #800

Merged
merged 1 commit into from Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/apis/index.ts
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
@@ -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
@@ -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
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
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
@@ -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
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
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
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
@@ -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)
})
})