现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!
常用库有 element、antd、iView、antd pro
,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。
接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步
。
需求:
自定义树
,使用antd树组件的checkbox
的时候,会发现选中某父级下的子级节点时候,其父级均为半选中状态,当我们获取其选中的key时,只有把其父级下所有子级全部选中,才能拿到其父级。自定义树,就是基于这个弊端,进行改造,选中某父级的子级,其父级均被选中。
antd tree(用于对比)
选中某一父级,下的子级
选中的key
js
const treeData: DataNode[] = [
{
title: '0-0',
key: '0-0',
children: [
{
title: '0-0-0',
key: '0-0-0',
children: [
{ title: '0-0-0-0', key: '0-0-0-0' },
{ title: '0-0-0-1', key: '0-0-0-1' },
{ title: '0-0-0-2', key: '0-0-0-2' },
],
},
{
title: '0-0-1',
key: '0-0-1',
children: [
{ title: '0-0-1-0', key: '0-0-1-0' },
{ title: '0-0-1-1', key: '0-0-1-1' },
{ title: '0-0-1-2', key: '0-0-1-2' },
],
},
{
title: '0-0-2',
key: '0-0-2',
},
],
},
{
title: '0-1',
key: '0-1',
children: [
{ title: '0-1-0-0', key: '0-1-0-0' },
{ title: '0-1-0-1', key: '0-1-0-1' },
{ title: '0-1-0-2', key: '0-1-0-2' },
],
},
{
title: '0-2',
key: '0-2',
},
];
自定义树-实现效果
选中子级,默认勾选所有父级
选中父级,默认勾选所有子级
高亮搜索
默认选中值(外部传入默认选中的key)
支持清除模式
功能点划分
- 自定义树组件选中子级,默认勾选所有父级
- 自定义树组件选中父级,默认勾选所有子级
- 自定义树组件,搜索关键字高亮
- 自定义树组件,默认选中
- 自定义树组件,支持清除模式
使用到组件(Antd 组件库哈)
- Tree
- Input
- Button
自定义树组件区域划分
- 搜索框
- 树组件
自定义树组件最终可支持配置项
js
interface SuCustomTreeSelectProps {
basePropName?: any // 转换的配置
emitTreeData?: any // 传递的树数据
defaultCheckKey?: any // 默认选中的key
treeOnCheck?: (checkKeys) => void // 树节点选中回调
maxTitleWidth?: number // 树展示宽度限制 默认280
delTreeMode?: boolean // 是否开启删除模式
delKeys?: any // 删除的key
setDelKeys?: any // set删除的key
}
自定义树组件布局
- 这里使用了Tailwind CSS
js
<CustomTreeSelectWrapper className="flex flex-col">
<div className="container">
<div className="flex w-full">
<div className="box-border flex-1">
<div>
<div className="flex items-center box-border py-3">
<div className="flex items-center mt-2 flex-1">
<Search
style={{ marginBottom: 8 }}
placeholder="请输入"
onSearch={onSearch}
allowClear
className="flex-1"
/>
</div>
</div>
</div>
<div style={{ height: '500px' }} className="mt-1 overflow-y-auto">
<Tree
checkable={!delTreeMode}
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onCheck={onCheck}
checkedKeys={checkedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={treeData}
switcherIcon={<DownOutlined />}
blockNode
checkStrictly
/>
</div>
</div>
</div>
</div>
</CustomTreeSelectWrapper>
自定义树组件-树节点数据(用于搜索高亮以及树数据处理)
js
/**
* @description: 根据搜索值的变化,处理树节点数据,完成高亮展示关键词文本
* @param {*} useMemo
* @return {*}
*/
const treeData = useMemo(() => {
const handleCustomTreeData = (list: any, count = 1) => {
const arr = []
// eslint-disable-next-line array-callback-return, consistent-return
list?.forEach(it => {
const key = keepIdProp ? it?.[idPropName] : it?.[keyPropName]
const value = keepIdProp ? it?.[idPropName] : it?.[keyPropName]
const strTitle = it[titlePropName] as string
const index = strTitle.indexOf(searchValue)
const beforeStr = strTitle.substring(0, index)
const afterStr = strTitle.slice(index + searchValue.length)
const title =
index > -1 ? (
<div className="flex justify-between w-full items-center">
<span
className="flex-1 text-ellipsis overflow-hidden whitespace-nowrap"
style={{ width: `${maxTitleWidth}px` }}
>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
{delTreeMode && (
<Button
type="text"
className="text-right"
onClick={() => deleteRightData(it.key, customTreeOriginData)}
size="small"
>
<DeleteOutlined />
</Button>
)}
</div>
) : (
<span className="flex-1">
<div className="flex justify-between w-full items-center">
<span
className="flex-1 text-ellipsis overflow-hidden whitespace-nowrap"
style={{ width: `${maxTitleWidth}px` }}
>
{strTitle}
</span>
{delTreeMode && (
<Button
type="text"
className="text-right"
onClick={() => deleteRightData(it.key, customTreeOriginData)}
size="small"
>
<DeleteOutlined />
</Button>
)}
</div>
</span>
)
if (it?.[childrenPropName] && it?.[childrenPropName]?.length) {
arr.push({
...it,
key,
title,
value,
children: handleCustomTreeData(it?.[childrenPropName], count + 1),
level: count,
})
} else {
arr.push({
...it,
key,
title,
value,
children: it?.[childrenPropName],
level: count,
})
}
})
setCustomTreeData(arr)
const finallyMapArr = handleTreeToFlatList(arr)
if (delTreeMode) {
const keys = finallyMapArr?.map(it => it?.key)
setExpandedKeys(keys)
}
return arr
}
return handleCustomTreeData(customTreeOriginData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue, customTreeOriginData])
功能点逐一拆解
自定义树组件选中子级,默认勾选所有父级
- 首先,我们需要获取当前选中节点信息(每次只会选中一个节点)
- 其次,需要准备好当前树数据的平铺数组,根据当前key的parentId依次寻找
- 注意的是,我们怎么知道到底上面还有没有父级呢?
- 我的做法时,前端处理树的时候,自己存一个level字段,表示当前层级(或许你后端会给你。。。笑嘻嘻)
- 原理就是,递归的时候,以count计数,每执行一次,代表层级加1,赋值给每一个树节点即可
- 最后,一个for循环,以level为起始值,i>1为结束,i--,去遍历平铺的树,找到便记录下来
- 拿捏
js
/**
* @description: 寻找当前节点的所有父级
* @param {*} key 当前选中的key
* @return {*} 所找到的父级keys
*/
const loopGetParentKey = key => {
// 当前key 信息
const curNodeInfo = customTreeFlatData?.filter(it => it?.key === key)
if (!curNodeInfo?.length) return []
const { level, parentId } = curNodeInfo[0]
const allParentKeys = []
const allParentNodes = []
let shouldFindParentKey = parentId
for (let i = level; i > 1; i--) {
// 找到当前符合的key
// eslint-disable-next-line no-loop-func
const findParentKeyInfo = customTreeFlatData?.find(it => it?.key === shouldFindParentKey)
// eslint-disable-next-line @typescript-eslint/no-shadow
const { key, parentId } = findParentKeyInfo
// 记录其父级并更新以及父级节点信息
allParentNodes.push(findParentKeyInfo)
allParentKeys.push(key)
shouldFindParentKey = parentId
}
return allParentKeys?.filter(it => it)
}
自定义树组件选中父级,默认勾选所有子级
- 首先,我们需要获取当前选中节点信息(每次只会选中一个节点)
- 其次,根据当前节点信息,判断是否有子级嘛
- 逐级找到,set一下当前选中的key
- 拿捏
js
/**
* @description: 寻找当前节点的所有子级
* @param {*} key 当前选中的key
* @return {*} 所找到的子级keys
*/
const loopGetChildKey = key => {
// 当前key 信息
const curNodeInfo = customTreeFlatData?.filter(it => it?.key === key)
if (!curNodeInfo?.length) return []
const { children } = curNodeInfo[0]
if (!children || !children?.length) return []
// 以该节点作为顶层 往下找自己的子级
const handleLoopChild = (childList = []) => {
return childList?.reduce((val, cur) => {
if (cur?.children && cur?.children?.length) {
const loopArr = handleLoopChild(cur?.children || [])
return [...val, ...loopArr, cur]
}
return [...val, cur]
}, [])
}
const resNodes = handleLoopChild(children)
const resKeys = resNodes?.map(it => it?.key)
return resKeys
}
自定义选中-选中的key,需要自己去处理
- 当我们选中某一节点的时候,我们需要关注当前节点的选中状态
- 如果点击选中,需要关注的key有:先前已选的,以及当前节点所关联的父级以及其所有的子级,以及当前节点本身
- 如果是取消选中,需要关注的key有:只取消当前节点以及该节点下存在子级
- 最后去重处理 避免存在key重复
js
/**
* @description: 自定义选中树节点
* @param {*} curCheckKey 当前选中的key
* @param {*} isCheck 选中状态
* @param {*} preCheckKey 前面已选key
* @return {*}
*/
// eslint-disable-next-line consistent-return
const handleCustomCheck = (curCheckKey, isCheck, preCheckKey = []) => {
// 从平铺当中找到符合的要求的key
let needCheckParentKeys = customTreeFlatData
// eslint-disable-next-line array-callback-return, consistent-return
.map(item => {
if (!isCheck) {
return null
}
// 从符合要求的key 当中寻找其所有父级 (向上寻找)
// 从符合要求的key 当中寻找其所有子级 (向下寻找)
if (item?.key === curCheckKey) {
const allChildKey = loopGetChildKey(curCheckKey)
const allParentKey = loopGetParentKey(curCheckKey)
return [...allChildKey, ...allParentKey]
}
return null
})
?.filter(it => it)
needCheckParentKeys = needCheckParentKeys?.flat()
if (isCheck) {
// 之前选中的 以及当前选中节点所关联父级以及其所有的子级 以及节点本身
const tempArr = preCheckKey
tempArr.push(...needCheckParentKeys, curCheckKey)
// 去重
const finallyArr = new Set(tempArr)
return Array.from([...finallyArr])
}
// 取消选中 只取消当前节点以及该节点下存在子级 一并取消
const allChildKey = loopGetChildKey(curCheckKey)
const allDelChildKey = [...allChildKey, curCheckKey]
const delCurKeyArr = preCheckKey.filter(it => !allDelChildKey.includes(it))
// 去重
const finallyArr = new Set(delCurKeyArr)
return Array.from([...finallyArr])
}
自定义树组件,搜索关键字高亮(基于前面useMemo 缓存树数据监听搜索值变化实现高亮)
- 搜索逻辑为,当title里面存在的搜索关键字时,展开当前节点,其余节点不展开,并关键字高亮
- 首先,需要从平铺的树中,去找到符合的树节点
- 其次,把这些符合的key,寻找其父级展开 antd 的展开key,存在某一子级,关联父级便可自动展开
js
/**
* @description: 只会寻找一次 antd 的展开key,存在某一子级,关联父级便可自动展开
* @param {React} key 符合条件key || 当前选中的key
* @param {any} tree 树数据
* @return {*}
*/
const getParentKey = (key: React.Key, tree: any[]): React.Key => {
let parentKey: React.Key
// 一层一层进行遍历
for (let i = 0; i < tree.length; i++) {
const node = tree[i]
// 如果当前节点存在孩子
if (node.children) {
// 如果孩子当中 找到其传进来的key 即当前节点 就是所需要找到父级节点
if (node.children.some(item => item.key === key)) {
parentKey = node.key
} else if (getParentKey(key, node.children)) {
// 否则循环往复
parentKey = getParentKey(key, node.children)
}
}
}
return parentKey!
}
自定义树组件,支持清除模式
- 这里主要是为了后续的自定义树穿梭框做的准备
- 往往右侧数据是,通过点击删除按钮,即可删除当前数据
- 开启删除模式,需要外部传入delKey,setDelKey,并且会更新选中的key ===> checkbox模式的选中与未选中
js
useEffect(() => {
const { checkedKeys, setCheckedKeys } = leftTreeRef.current || {}
const filterKeys = checkedKeys?.filter(it => !delKeys.includes(it))
setCheckedKeys(filterKeys)
}, [delKeys])
删除使用示例
<CustomTreeSelect
defaultCheckKey={[]}
treeOnCheck={onCheckBoxChange}
emitTreeData={rightOriginData}
delKeys={delKeys}
setDelKeys={setDelKeys}
delTreeMode
ref={rightTreeRef}
basePropName={{
...basePropName,
// 转换以后的字段叫做children
childrenPropName: 'children',
}}
/>
核心
自定义组件最核心的逻辑:
1.选中与未选中时,需要追加或删减的key
2.选中父级时,寻找子级(向下寻找)
3.选中子级时,寻找其父级(向上寻找)
结束
都看到这里了,不留点痕迹,是怕我发现么?
上一篇
自建"IT兵器库",你值得一看!第三篇 - 掘金 (juejin.cn)