veui
Version:
Baidu Enterprise UI for Vue.js.
152 lines (135 loc) • 4.48 kB
JavaScript
/**
* 用户直接使用 Provider, 接受一个 value prop 定义 context
* 采用 Provider(functional) + ProviderImpl 方式实现的原因:
* 1. 支持给多个子节点传递 context : <Provider><a/><b/></Provider>
* 2. functional 组件定义 provide 无效(inject 有效)
*/
import {
uniqueId,
isPlainObject,
defaults,
reduce,
set,
cloneDeep
} from 'lodash'
const DEEP_KEY_RE = /^([^.]+\.[^.]+)\.(.+)$/
export function createContext (name, defaultValue) {
const realName = `${name || 'veui'}-provider`
const contextId = `__${uniqueId(realName)}`
function useProvider (valueKey = 'value', { override } = {}) {
return {
inject: {
[contextId]: {
from: contextId,
default: () => () => undefined
}
},
provide () {
return {
// provide 一个函数,该函数在消费方的 computed 调用,这样保证最终值依赖每个 provider 的 this.value
[contextId]: () => {
const parentContextValue = this[contextId]()
const isObjParent = isPlainObject(parentContextValue)
const isObjSelf = isPlainObject(this[valueKey])
// this.value['button.ui'] 覆盖 parentContextValue['button.ui.*']
if (isObjParent && isObjSelf) {
Object.keys(parentContextValue).forEach((key) => {
const match = key.match(DEEP_KEY_RE)
if (match && typeof this[valueKey][match[1]] !== 'undefined') {
delete parentContextValue[key]
}
})
}
const base = override ? override.call(this) : {}
return isObjParent && isObjSelf
? defaults(base, this[valueKey], parentContextValue) // 和上层合并
: isObjSelf
? defaults(base, this[valueKey])
: this[valueKey] // 无法合并,则以最近的 provider 为准
}
}
}
}
}
const ProviderImpl = {
name: realName,
uiTypes: ['transparent'],
mixins: [useProvider()],
props: {
// eslint-disable-next-line vue/require-prop-types
value: {}
},
render () {
return this.$slots.default
}
}
const Provider = {
functional: true,
// 这里实际上接受一个 value prop,用来传递 context,但是因为直接透传给 ProviderImpl,所以不用声明了
render: (h, context) => wrapChildren(h, context, ProviderImpl)
}
function useConsumer (injectionKey) {
return {
inject: {
[contextId]: {
from: contextId,
default: () => () => undefined
}
},
computed: {
[injectionKey] () {
const value = this[contextId]()
const isObj = isPlainObject(value)
const [toMerge, deepKeys] = reduce(
isObj ? value : undefined,
(acc, val, key) => {
const match = key.match(DEEP_KEY_RE)
if (match) {
acc[1].push(match)
} else {
acc[0][key] = val
}
return acc
},
[{}, []]
)
const defaultVal =
typeof defaultValue === 'function' ? defaultValue() : defaultValue
let result = typeof value === 'undefined' ? defaultVal : value
if (isObj && isPlainObject(defaultVal)) {
// 消费方获取 context 值时和初始值做合并, 先不要 deepKeys
result = defaults({}, toMerge, defaultVal)
}
// deepKeys 设置进去: button.icons.loading -> set(button.icons, loading, value)
if (deepKeys.length) {
result = { ...result }
deepKeys.forEach(([key, prefix, rest]) => {
result[prefix] = result[prefix] ? cloneDeep(result[prefix]) : {}
set(result[prefix], rest, value[key])
})
}
return result
}
}
}
}
const Consumer = {
mixins: [useConsumer('context')],
render () {
return this.$scopedSlots.default(this.context)
}
}
return { useProvider, useConsumer, Provider, Consumer }
}
function wrapChildren (h, { data, children }, Provider) {
const value = data.attrs ? data.attrs.value : undefined
return children.map((child) =>
h(
Provider,
{
attrs: { value } // 分开避免相互影响
},
[child]
)
)
}