Skip to content

Commit 1e6e3a9

Browse files
authored
feat: implement api useSlots and useAttrs (#800)
1 parent 72a878d commit 1e6e3a9

File tree

10 files changed

+142
-54
lines changed

10 files changed

+142
-54
lines changed

src/apis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export {
2525
getCurrentScope,
2626
onScopeDispose,
2727
} from './effectScope'
28+
export { useAttrs, useSlots } from './setupHelpers'

src/apis/setupHelpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getCurrentInstance, SetupContext } from '../runtimeContext'
2+
import { warn } from '../utils'
3+
4+
export function useSlots(): SetupContext['slots'] {
5+
return getContext().slots
6+
}
7+
8+
export function useAttrs(): SetupContext['attrs'] {
9+
return getContext().attrs
10+
}
11+
12+
function getContext(): SetupContext {
13+
const i = getCurrentInstance()!
14+
if (__DEV__ && !i) {
15+
warn(`useContext() called without active instance.`)
16+
}
17+
return i.setupContext!
18+
}

src/component/componentOptions.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,9 @@
11
import Vue, { VNode, ComponentOptions as Vue2ComponentOptions } from 'vue'
2+
import { SetupContext } from '../runtimeContext'
23
import { Data } from './common'
34
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
4-
import { ComponentInstance, ComponentRenderProxy } from './componentProxy'
5+
import { ComponentRenderProxy } from './componentProxy'
56
export { ComponentPropsOptions } from './componentProps'
6-
export interface SetupContext {
7-
readonly attrs: Data
8-
readonly slots: Readonly<{ [key in string]?: (...args: any[]) => VNode[] }>
9-
10-
/**
11-
* @deprecated not available in Vue 3
12-
*/
13-
readonly parent: ComponentInstance | null
14-
15-
/**
16-
* @deprecated not available in Vue 3
17-
*/
18-
readonly root: ComponentInstance
19-
20-
/**
21-
* @deprecated not available in Vue 3
22-
*/
23-
readonly listeners: { [key in string]?: Function }
24-
25-
/**
26-
* @deprecated not available in Vue 3
27-
*/
28-
readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }
29-
30-
emit(event: string, ...args: any[]): void
31-
}
327

338
export type ComputedGetter<T> = (ctx?: any) => T
349
export type ComputedSetter<T> = (v: T) => void

src/component/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export { defineComponent } from './defineComponent'
22
export { defineAsyncComponent } from './defineAsyncComponent'
33
export {
44
SetupFunction,
5-
SetupContext,
65
ComputedOptions,
76
MethodOptions,
87
ComponentPropsOptions,

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ export const version = __VERSION__
66

77
export * from './apis'
88
export * from './component'
9-
export { getCurrentInstance, ComponentInternalInstance } from './runtimeContext'
9+
export {
10+
getCurrentInstance,
11+
ComponentInternalInstance,
12+
SetupContext,
13+
} from './runtimeContext'
1014

1115
export default Plugin
1216

src/mixin.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import type { VueConstructor } from 'vue'
2-
import {
3-
ComponentInstance,
4-
SetupContext,
5-
SetupFunction,
6-
Data,
7-
} from './component'
2+
import { ComponentInstance, SetupFunction, Data } from './component'
83
import { isRef, isReactive, toRefs, isRaw } from './reactivity'
94
import {
105
isPlainObject,
@@ -24,7 +19,11 @@ import {
2419
resolveScopedSlots,
2520
asVmProperty,
2621
} from './utils/instance'
27-
import { getVueConstructor } from './runtimeContext'
22+
import {
23+
getVueConstructor,
24+
SetupContext,
25+
toVue3ComponentInstance,
26+
} from './runtimeContext'
2827
import { createObserver, reactive } from './reactivity/reactive'
2928

3029
export function mixin(Vue: VueConstructor) {
@@ -53,7 +52,9 @@ export function mixin(Vue: VueConstructor) {
5352
if (render) {
5453
// keep currentInstance accessible for createElement
5554
$options.render = function (...args: any): any {
56-
return activateCurrentInstance(vm, () => render.apply(this, args))
55+
return activateCurrentInstance(toVue3ComponentInstance(vm), () =>
56+
render.apply(this, args)
57+
)
5758
}
5859
}
5960

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

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

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

9698
let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
97-
activateCurrentInstance(vm, () => {
99+
activateCurrentInstance(instance, () => {
98100
// make props to be fake reactive, this is for `toRefs(props)`
99101
binding = setup(props, ctx)
100102
})
@@ -105,9 +107,8 @@ export function mixin(Vue: VueConstructor) {
105107
const bindingFunc = binding
106108
// keep currentInstance accessible for createElement
107109
vm.$options.render = () => {
108-
// @ts-expect-error
109110
resolveScopedSlots(vm, ctx.slots)
110-
return activateCurrentInstance(vm, () => bindingFunc())
111+
return activateCurrentInstance(instance, () => bindingFunc())
111112
}
112113
return
113114
} else if (isPlainObject(binding)) {
@@ -228,23 +229,25 @@ export function mixin(Vue: VueConstructor) {
228229
})
229230
})
230231

232+
let propsProxy: any
231233
propsReactiveProxy.forEach((key) => {
232234
let srcKey = `$${key}`
233235
proxy(ctx, key, {
234236
get: () => {
235-
const data = reactive({})
237+
if (propsProxy) return propsProxy
238+
propsProxy = reactive({})
236239
const source = vm[srcKey]
237240

238241
for (const attr of Object.keys(source)) {
239-
proxy(data, attr, {
242+
proxy(propsProxy, attr, {
240243
get: () => {
241244
// to ensure it always return the latest value
242245
return vm[srcKey][attr]
243246
},
244247
})
245248
}
246249

247-
return data
250+
return propsProxy
248251
},
249252
set() {
250253
__DEV__ &&

src/runtimeContext.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,38 @@ export type EmitFn<
134134
}[Event]
135135
>
136136

137+
export type Slots = Readonly<InternalSlots>
138+
139+
export interface SetupContext<E = EmitsOptions> {
140+
attrs: Data
141+
slots: Slots
142+
emit: EmitFn<E>
143+
/**
144+
* @deprecated not available in Vue 2
145+
*/
146+
expose: (exposed?: Record<string, any>) => void
147+
148+
/**
149+
* @deprecated not available in Vue 3
150+
*/
151+
readonly parent: ComponentInstance | null
152+
153+
/**
154+
* @deprecated not available in Vue 3
155+
*/
156+
readonly root: ComponentInstance
157+
158+
/**
159+
* @deprecated not available in Vue 3
160+
*/
161+
readonly listeners: { [key in string]?: Function }
162+
163+
/**
164+
* @deprecated not available in Vue 3
165+
*/
166+
readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }
167+
}
168+
137169
/**
138170
* We expose a subset of properties on the internal instance as they are
139171
* useful for advanced external libraries and tools.
@@ -179,6 +211,11 @@ export declare interface ComponentInternalInstance {
179211
* @internal
180212
*/
181213
scope: EffectScope
214+
215+
/**
216+
* @internal
217+
*/
218+
setupContext: SetupContext | null
182219
}
183220

184221
export function getCurrentInstance() {
@@ -190,7 +227,7 @@ const instanceMapCache = new WeakMap<
190227
ComponentInternalInstance
191228
>()
192229

193-
function toVue3ComponentInstance(
230+
export function toVue3ComponentInstance(
194231
vm: ComponentInstance
195232
): ComponentInternalInstance {
196233
if (instanceMapCache.has(vm)) {

src/utils/helper.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ComponentInternalInstance,
55
getCurrentInstance,
66
getVueConstructor,
7+
Slot,
78
} from '../runtimeContext'
89
import { warn } from './utils'
910

@@ -38,8 +39,8 @@ export function isComponentInstance(obj: any) {
3839
return Vue && obj instanceof Vue
3940
}
4041

41-
export function createSlotProxy(vm: ComponentInstance, slotName: string) {
42-
return (...args: any) => {
42+
export function createSlotProxy(vm: ComponentInstance, slotName: string): Slot {
43+
return ((...args: any) => {
4344
if (!vm.$scopedSlots[slotName]) {
4445
if (__DEV__)
4546
return warn(
@@ -50,7 +51,7 @@ export function createSlotProxy(vm: ComponentInstance, slotName: string) {
5051
}
5152

5253
return vm.$scopedSlots[slotName]!.apply(vm, args)
53-
}
54+
}) as Slot
5455
}
5556

5657
export function resolveSlots(

src/utils/instance.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import vmStateManager from './vmStateManager'
33
import {
44
setCurrentInstance,
55
getCurrentInstance,
6-
setCurrentVue2Instance,
6+
ComponentInternalInstance,
7+
InternalSlots,
78
} from '../runtimeContext'
89
import { Ref, isRef, isReactive } from '../apis'
910
import { hasOwn, proxy, warn } from './utils'
@@ -102,7 +103,7 @@ export function updateTemplateRef(vm: ComponentInstance) {
102103

103104
export function resolveScopedSlots(
104105
vm: ComponentInstance,
105-
slotsProxy: { [x: string]: Function }
106+
slotsProxy: InternalSlots
106107
): void {
107108
const parentVNode = (vm.$options as any)._parentVnode
108109
if (!parentVNode) return
@@ -129,14 +130,14 @@ export function resolveScopedSlots(
129130
}
130131

131132
export function activateCurrentInstance(
132-
vm: ComponentInstance,
133-
fn: (vm_: ComponentInstance) => any,
133+
instance: ComponentInternalInstance,
134+
fn: (instance: ComponentInternalInstance) => any,
134135
onError?: (err: Error) => void
135136
) {
136137
let preVm = getCurrentInstance()
137-
setCurrentVue2Instance(vm)
138+
setCurrentInstance(instance)
138139
try {
139-
return fn(vm)
140+
return fn(instance)
140141
} catch (err) {
141142
if (onError) {
142143
onError(err)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
createApp,
3+
defineComponent,
4+
SetupContext,
5+
useAttrs,
6+
useSlots,
7+
} from '../../../src'
8+
9+
describe('SFC <script setup> helpers', () => {
10+
// test('useSlots / useAttrs (no args)', () => {
11+
// let slots: SetupContext['slots'] | undefined
12+
// let attrs: SetupContext['attrs'] | undefined
13+
// const Comp = {
14+
// setup() {
15+
// slots = useSlots()
16+
// attrs = useAttrs()
17+
// return () => {}
18+
// }
19+
// }
20+
// const passedAttrs = { id: 'foo' }
21+
// const passedSlots = {
22+
// default: () => {},
23+
// x: () => {}
24+
// }
25+
// const root = document.createElement('div')
26+
// const vm = createApp(Comp).mount(root)
27+
// expect(typeof slots!.default).toBe('function')
28+
// expect(typeof slots!.x).toBe('function')
29+
// expect(attrs).toMatchObject(passedAttrs)
30+
// })
31+
32+
test('useSlots / useAttrs (with args)', () => {
33+
let slots: SetupContext['slots'] | undefined
34+
let attrs: SetupContext['attrs'] | undefined
35+
let ctx: SetupContext | undefined
36+
const Comp = defineComponent({
37+
setup(_, _ctx) {
38+
slots = useSlots()
39+
attrs = useAttrs()
40+
ctx = _ctx
41+
return () => {}
42+
},
43+
})
44+
const root = document.createElement('div')
45+
createApp(Comp, { foo: 'bar' }).mount(root)
46+
expect(slots).toBe(ctx!.slots)
47+
expect(attrs).toBe(ctx!.attrs)
48+
})
49+
})

0 commit comments

Comments
 (0)