veui
Version:
Baidu Enterprise UI for Vue.js.
181 lines (159 loc) • 4.97 kB
JavaScript
import { upperFirst, isString, isPlainObject, reduce, find } from 'lodash'
import { getModelEvent } from '../utils/helper'
let options = {
methods: {
isControlled (prop) {
return isControlled(this, prop)
},
// 使用方法而非直接赋值:受控时赋值并未直接生效,而仅仅 emit 事件而已,直接让使用方使用赋值违反直觉
// commit 可以额外的给事件传递更多的参数
commit (prop, value, ...args) {
let def = find(this._controlledProps, (i) => i.prop === prop)
if (def) {
computedSetter(this, value, def, ...args)
return
}
throw new Error(
`[veui-controllable] Unkown prop key: \`${prop}\` on committing.`
)
}
}
}
const errorMsg =
'[veui-controllable] Prop config must be either a string, an object or an object array.'
/**
* 自动将对应的 prop 转换成受控的
* @param {string|Object|[string|Object]} props - 定义的受控props
* @return 转换后的 mixin
*/
export default function useControllable (props) {
if (!props) {
// expose default methods
return options
}
if (isString(props)) {
props = [
{
prop: props
}
]
} else if (isPlainObject(props)) {
props = [props]
}
if (!Array.isArray(props)) {
throw new Error(errorMsg)
}
let result = props.reduce(
(result, def) => {
if (isString(def)) {
def = { prop: def }
} else if (!isPlainObject(def)) {
console.error(errorMsg)
return result
} else if (!def.prop) {
console.error(
'[controllable] the `prop` property is required when prop config item is an object.'
)
return result
}
// store prop definitions
result.normalized.push(def)
// { prop, local, computed, event, get, set }
let { prop, local, get, set } = def
if (local !== false) {
result.data[getLocalName(def)] = (vm) => vm[prop]
}
result.computed[getRealName(def)] = {
get () {
if (get === false) {
throw new Error("[veui-controllable] Can't access disabled getter.")
}
return computedGetter(this, def)
},
set (value) {
// 一般可以用来禁用 assignment,而强制使用 vm.commit !
// 对于 v-model/.sync,这个 set 还是有点用处
if (set === false) {
throw new Error("[veui-controllable] Can't access disabled setter.")
}
return computedSetter(this, value, def)
}
}
return result
},
{ computed: {}, data: {}, normalized: [] }
)
return {
...options,
computed: result.computed,
beforeCreate () {
// 比如 Menu 组件在 MenuMixin 和 Menu 里面都用了 controllable,所以要受控 prop 的定义都 merge 起来
// 后面会通过 prop name 来取
// 暂时不放 created 里面,因为 immediate watcher 比 created 早
this._controlledProps = this._controlledProps
? [...this._controlledProps, ...result.normalized]
: result.normalized
},
data () {
return reduce(
result.data,
(res, fn, key) => {
res[key] = fn(this)
return res
},
{}
)
}
}
}
function computedGetter (vm, def) {
let { get } = def
return get ? get.call(vm, getReal(vm, def)) : getReal(vm, def)
}
function computedSetter (vm, value, def, ...args) {
let { set } = def
// 值不同则更新, 若不更新:不设置 local,不 emit 事件
let oldReal = getReal(vm, def)
if (oldReal !== value) {
return set
? set.call(vm, value, (val) => setReal(vm, val, def, ...args))
: setReal(vm, value, def, ...args)
}
}
function getReal (vm, { prop, local } = {}) {
return vm.isControlled(prop)
? vm[prop]
: local
? vm[local]
: vm[`local${upperFirst(prop)}`]
}
function setReal (vm, value, def = {}, ...args) {
const { prop, local, event } = def
// false 则认为忽略对应的操作
if (local !== false) {
vm[getLocalName(def)] = value
}
if (event !== false) {
let modelEvent = getModelEvent(vm)
if (event !== modelEvent) {
vm.$emit(`update:${prop}`, value, ...args)
}
if (event) {
vm.$emit(event, value, ...args)
}
}
}
function getLocalName ({ prop, local } = {}) {
return local || `local${upperFirst(prop)}`
}
function getRealName ({ prop, computed } = {}) {
return computed || `real${upperFirst(prop)}`
}
function isControlled (vm, prop) {
// 受控定义:显式传了 prop 且传的不是 undefined 就认为受控了
// 为了响应性,因为下面 in propsData 没有响应性,用 void() 包裹提示 CodeQL 语句有副作用
// eslint-disable-next-line no-void, space-unary-ops
void vm[prop]
let propsData = vm.$options.propsData
return prop in propsData && typeof propsData[prop] !== 'undefined'
}