-
-
Notifications
You must be signed in to change notification settings - Fork 35
/
createProvide.ts
137 lines (114 loc) · 5.08 KB
/
createProvide.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type { ComputedRef, InjectionKey } from 'vue'
import { computed, defineComponent, inject, provide, reactive } from 'vue'
function createProvide<ProvideValueType extends object | null>(
rootComponentName: string,
defaultProvide?: ProvideValueType,
) {
const Provide = Symbol(rootComponentName)
const Provider = defineComponent({
name: `${rootComponentName}Provider`,
inheritAttrs: false,
setup(props, { attrs, slots }) {
const value = reactive(attrs) as ProvideValueType
provide(Provide, value)
if (!slots || !slots.default)
throw new Error(`\`${rootComponentName}Provider\` must have a default slot :(`)
return () => slots.default?.()
},
})
function useContext(consumerName: string) {
const provide = inject(Provide)
if (provide)
return provide
if (defaultProvide !== undefined)
return defaultProvide
// if a defaultProvide wasn't specified, it's a required provide.
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``)
}
return [Provider, useContext] as const
}
/* -------------------------------------------------------------------------------------------------
* createProvideScope
* ----------------------------------------------------------------------------------------------- */
type VueProvide<C> = {
key: InjectionKey<C>
value: C
}
type Scope<C = any> = { [scopeName: string]: VueProvide<C>[] } | undefined
type ScopeHook = (scope: Scope) => ComputedRef<{ [__scopeProp: string]: Scope }>
interface CreateScope {
scopeName: string
(): ScopeHook
}
function createProvideScope(scopeName: string, createProvideScopeDeps: CreateScope[] = []) {
let defaultProviders: any[] = []
/* -----------------------------------------------------------------------------------------------
* createContext
* --------------------------------------------------------------------------------------------- */
function createProvide<ProvideValueType extends object | null>(
rootComponentName: string,
defaultValue?: ProvideValueType,
) {
const BaseProvideKey: InjectionKey<ProvideValueType | null> = rootComponentName as any
// const BaseProvide = provide(BaseProvideKey, defaultValue)
const BaseScope = { key: BaseProvideKey, value: defaultValue } as VueProvide<ProvideValueType | null>
const index = defaultProviders.length
defaultProviders = [...defaultProviders, { [scopeName]: [{ key: BaseProvideKey, value: defaultValue }] }]
function Provider(
props: ProvideValueType & { scope: Scope<ProvideValueType> },
) {
const { scope, ...context } = props as any
const Provide = scope?.[scopeName][index] || BaseScope.key as ProvideValueType
const value = computed<ProvideValueType>(() => context)
provide(Provide, value)
}
function useInject(consumerName: string, scope: Scope<ProvideValueType | undefined>): ComputedRef<ProvideValueType> {
const Provide = scope?.[scopeName]?.[index] || BaseScope
const provide = inject(Provide.key)
if (provide)
return provide as ComputedRef<ProvideValueType>
if (defaultValue !== undefined)
return computed(() => defaultValue)
// // if a defaultProvide wasn't specified, it's a required provide.
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``)
}
return [Provider, useInject] as const
}
/* -----------------------------------------------------------------------------------------------
* createScope
* --------------------------------------------------------------------------------------------- */
const createScope: CreateScope = () => {
const scopeProviders = defaultProviders[0]
return function useScope(scope: Scope) {
const providers = scope?.[scopeName] || scopeProviders
return computed(() => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: providers } }))
}
}
createScope.scopeName = scopeName
return [createProvide, composeContextScopes(createScope, ...createProvideScopeDeps)] as const
}
function composeContextScopes(...scopes: CreateScope[]) {
const baseScope = scopes[0]
if (scopes.length === 1)
return baseScope
const createScope: CreateScope = () => {
const scopeHooks = scopes.map(createScope => ({
useScope: createScope(),
scopeName: createScope.scopeName,
}))
return function useComposedScopes(overrideScopes) {
const nextScopes = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {
// We are calling a hook inside a callback which React warns against to avoid inconsistent
// renders, however, scoping doesn't have render side effects so we ignore the rule.
const scopeProps = useScope(overrideScopes)
const currentScope = computed(() => scopeProps.value[`__scope${scopeName}`])
return { ...nextScopes, ...currentScope }
}, {})
return computed(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }))
}
}
createScope.scopeName = baseScope.scopeName
return createScope
}
export { createProvide, createProvideScope }
export type { CreateScope, Scope }