现在的组件库都会包含些相同的基础组件,功能大差不差,只是不同UI规范下的具体实现。这些基础组件基本能满足大部分的开发需求。 但世上无银弹,有时我们需要对组件做细微的调整可能是功能上的,可能是UI上的,例如 tab切换,需要自定义标签头,修改标签布局,显示或隐藏等,这些功能组件都是在大致的功能基础上做的定向扩展或修改。如果每次都重新编写一套逻辑就显得累赘,繁琐。
在vue3提供hook的逻辑复用方式后,我们能通过hook将公共逻辑抽离出来提高复用, 小的功能业务逻辑可以,基础组件交互逻辑当然也可以。 这里就遇到的一个菜单的单选,复选需求, 为了能以后快速开发类似的组件功能。尝试将逻辑封装成hook。 以hook提供的API 在实现不同的组件。
需求
开始需要明确我们需要的基础功能点,功能尽量基础才能保证通用性,后续的功能也能在基础功能上做扩展.
- 单选
- 多选
- 跨组件通信
单选,多选是基础能力, 跨组件是为了将实际的值控制与独立的 checkItem 分离,这样checkItem可以不受 html 层级限制
可以多种不同的checkItem切换或嵌套使用
checkContainer -> [ checkItemType1, checkItemType2 ]
使用例子
实现多选组件
容器组件 chekcboxContainer
<script setup>
import { useCheckboxContainer } from '@/hooks/useCheckbox'
import { toRefs } from 'vue'
import { PROVIDE_KEY } from './common'
const props = defineProps({
value: {
type: Array,
default: () => ([])
}
})
const emit = defineEmits(['input', 'change'])
const { value } = toRefs(props)
// 绑定 v-model 等事件
function selectHandler (selected) {
emit('input', selected)
emit('change', selected)
}
// 调用钩子逻辑
useCheckboxContainer({
setCb: selectHandler,
provideKey: PROVIDE_KEY,
valueRef: value
})
</script>
<template>
<div class="container">
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.container{
overflow: hidden;
border-radius: 4px;
border: 1px solid #00385a;
}
</style>
子项 chekcboxItem
<script setup>
import { ref } from 'vue'
import { useCheckboxItem } from '@/hooks/useCheckbox'
import { PROVIDE_KEY } from './common'
const emit = defineEmits(['change'])
const props = defineProps({
label: {
type: String,
default: ''
},
value: {
type: [Number, String],
default: ''
}
})
const { value, label } = toRefs(props)
const {
active,
pushItem,
updateActive
} = useCheckboxItem({
injectKey: PROVIDE_KEY,
value: unref(value)
})
// 先容器注册子项
pushItem(unref(value), {
value: unref(value),
label: unref(label)
})
// 绑定激活样式类
const cls = computed(() => {
return {
active: unref(active)
}
})
// 触发选择事件
function clickHandler(){
updateActive(unref(value))
emit('change', value)
}
</script>
<template>
<div class='checkbox-item' :class="cls" @click="clickHandler">
<slot>
{{ label }}
</slot>
</div>
</template>
<style lang='scss' scoped>
.checkbox-item{
padding: 8px 16px;
border: 1px solid #eee;
border-radius: 4px;
color: #333;
}
.active{
background: orange;
color: #fff;
}
</style>
组件使用
const selected = ref([])
<ChekcboxContainer v-model='selected'>
<ChekcboxItem value='red'> red </ChekcboxItem>
<ChekcboxItem value='orange'> orange </ChekcboxItem>
<ChekcboxItem value='blue'> blue </ChekcboxItem>
</ChekcboxContainer>
useCheckbox 实现
这里将功能才分到 useCheckboxContainer useCheckboxItem 两个hook中, useCheckboxContainer 包含主要逻辑 useCheckboxItem 用于触发选择,显示激活状态
import useBool from '../useBool'
import { shallowRef, inject, provide, computed, unref, watch } from 'vue'
import {
unique,
merge
} from './utils'
/**
* 新旧响应值标识符,防止无限循环更新组件内外值
*/
export const OLD_ACTIVE_VALUE_MAKR = Symbol('OLD_ACTIVE_VALUE_MAKR')
export const DEFAULT_SIGN = Symbol('DEFAULT_SIGN')
export const DEFAULT_CONTAINER_OPTIONS = {
provideKey: DEFAULT_SIGN
}
/**
* 多选容器钩子
* @summary
* 提供 1.多选 2.全选
* 通过依赖注入,保证子项与容器的通信。所以需要提供依赖注入标识
* @param options
* @param options.provideKey 依赖注入标识
* @param options.valueRef props 接收的已选列表
* @param options.setCb 更新已选值回调
* @returns [ state, tools ] 返回内部状态,工具函数
* state:
* - allCheck 全选状态
* - activeList 已选列表
* tools:
* - closeValueWatch 清除 valueRef 监听
* - updateActive 更新当前已选项, 1.已选列表无已选时,添加。 2.已选列表存在时,移除
* - pushItem 向容器中,注册子项信息
* - checkAll 全选
* - clearAll 全清空
* - setAllCheck allCheck设为true
* - removeAllCheck allCheck设为false
* - updateAllCheck 通过已选项更新 allCheck 状态
* - closeAllCheckStatusWatch 清除 allCheck 监听
*
*/
export function useCheckboxContainer (options = {}) {
const { provideKey, valueRef, setCb } = merge(DEFAULT_CONTAINER_OPTIONS, options)
const activeList = shallowRef([])
const [allCheck, { setTrue: setAllCheck, setFalse: removeAllCheck }] = useBool()
const allItem = new Map([])
function pushItem (key, item) {
allItem.set(key, item)
}
function checkAll () {
activeList.value = [...allItem.keys()]
setAllCheck()
}
function clearAll () {
activeList.value = []
removeActive()
}
function addSign (list) {
list[OLD_ACTIVE_VALUE_MAKR] = true
return list
}
function hasItem (item) {
return unref(activeList).includes(item)
}
function addActive (value) {
activeList.value = unique([...unref(activeList), value])
}
function removeActive (value) {
activeList.value = unref(activeList).filter(i => i !== value)
}
function updateActive (value) {
hasItem(value) ? removeActive(value) : addActive(value)
updateAllCheck()
setCb && setCb(addSign([...unref(activeList)]))
}
function updateAllCheck () {
allCheck.value = unref(activeList).length === [...allItem.keys()].length
}
const closeAllCheckStatusWatch = watch(allCheck, (newVal, oldVal) => {
if (newVal === oldVal) {
return
}
newVal ? checkAll() : clearAll()
})
// 同步预设值 or 外部动态修改
const closeValueWatch = watch(valueRef, (newVal) => {
if (newVal[OLD_ACTIVE_VALUE_MAKR] && newVal.length === unref(activeList).length) {
return
}
activeList.value = [...unref(valueRef)]
}, { immediate: true })
provide(provideKey, {
hasItem,
activeList,
updateActive,
pushItem
})
return [ { allCheck, activeList }, { closeValueWatch, updateActive, pushItem, checkAll, clearAll, setAllCheck, removeAllCheck, updateAllCheck, closeAllCheckStatusWatch } ]
}
export const DEFAULT_ITEM_OPTIONS = {
injectKey: DEFAULT_SIGN
}
/**
* 多选子项钩子
* @param options
* @param options.injectKey 依赖注入标识
* @param options.value 当前项值or标识
* @returns
* - active 当前项是否已选
* - hasItem 已选判断函数
* - activeList 已选列表
* - updateActive 更新已选项
* - pushItem 向容器注册子项
*/
export function useCheckboxItem (options = {}) {
const _options = merge(DEFAULT_ITEM_OPTIONS, options)
const {
injectKey,
value
} = _options
const injecteStore = inject(injectKey)
const active = computed(() => {
return injecteStore.hasItem(value)
})
return {
active,
...injecteStore
}
}
utils
export function unique (list) {
return Array.from(new Set(list))
}
export function isUndefined (v) {
return v === undefined
}
// 混合非 undefined 属性
export function merge (source = {}, target = {}) {
const keys = new Set([...Object.keys(source), ...Object.keys(target)])
return [...keys].reduce((acc, prop) => {
return {
...acc,
[prop]: isUndefined(source[prop]) ? target[prop] : source[prop]
}
}, {})
}