React 组件封装之 Tree 树形控件
前端处理的数据越来越复杂,嵌套的树形结构数据也越来越多,所以自己封装一个树形控件非常有必要,可以练习自己对树形控件的理解和数据的操作联系。
Tree 树形结构数据
这就是一个简单的树形结构数据
const Data = [
{
title: '一级菜单',
key: '0',
children: [
{
title: '二级菜单0',
key: '0-0',
disabled: true,
children: [
{
title: '三级菜单0',
key: '0-0-0',
disableCheckbox: true,
},
{
title: '三级菜单1',
key: '0-0-1',
},
],
},
{
title: '二级菜单1',
key: '0-1',
children: [{ title: <span style={{ color: '#1677ff' }}>菜单</span>, key: '0-0-1-0' }],
},
],
},
];
封装流程
- 源代码
下面是一个源代码 里面有注释,标注我封装组件时的一个流程,代码难懂的理解,本人技术一般,见谅。
import React, { FC, useState, useRef, memo } from 'react';
import { TreeProps } from './interface';
import Css from './index.module.less';
import Icon from '../Icon';
const Tree: FC<TreeProps> = memo(({ treeData = [], handleChange, a, left }) => {
let [keys, setKeys] = useState([]);
const sonRef = useRef(null);
// 点击事件
const handle = (e: any, item: any) => {
// 判断是否传入回调函数
handleChange ? handleChange(item) : null;
// 判断是否有子组件 是否需要展开, 不需要展开直接结束函数
if (!item.children || (e.target.localName !== 'span' && e.target.localName !== 'svg')) {
return;
}
let newKeys: any = keys;
let index = newKeys.findIndex((v: any) => v === item.key);
// 获取需要展开的dom元素
let ref = e.target.parentNode.nextSibling;
// 判断点击是否含有key值 如果已有key值,说明元素已展开
if (index >= 0) {
newKeys.splice(index, 1);
// 箭头旋转会原始角度
e.target.parentNode.children[0].style.transform = 'rotate(0deg)';
// 收缩元素
ref.style.height = 0 + 'px';
// 收缩删除类名
ref.classList.remove(Css['tree_show']);
// 如果没有key值,说明元素未展开
} else {
newKeys.push(item.key);
// 箭头旋转90度
e.target.parentNode.children[0].style.transform = 'rotate(90deg)';
// 展开元素
ref.style.height = ref.scrollHeight + 'px';
// 点击的非一级菜单高度自适应
if (a) {
a.current.style.height = 'auto';
}
// 展开添加类名
ref.classList.add(Css['tree_show']);
}
// 把修改后keys值更新
setKeys(newKeys);
};
return (
<div className={Css['tree_father']} style={{ marginLeft: left ? 1 * left + 'px' : '0px' }}>
{treeData.map((item: any) => {
return (
<div key={item.key}>
{/* 标题 */}
<div className={Css['title']} onClick={(e) => handle(e, item)}>
{item.children ? (
<Icon name={'arrow-rightjiantou-you2'} className={Css['icon']} size={14} />
) : null}
<span className={Css['title_text']}>{item.title}</span>
</div>
{/* 子菜单 */}
{item.children ? (
<div className={Css['tree_son']} ref={sonRef}>
<Tree
treeData={item.children}
left={left ? left : 16}
// 上一级元素
a={sonRef}
// 自定义事件
handleChange={handleChange ? handleChange : () => { }}
/>
</div>
) : null}
</div>
);
})}
</div>
);
});
export default Tree;
- 样式
使用到的样式文件
.tree_father {
display: inline-block;
user-select: none;
.title {
display: flex;
align-items: center;
padding: 2px;
}
.icon {
transition: all 0.3s;
}
.title_text {
display: block;
margin-left: 5px;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
}
.tree_show {
height: auto;
transition: all 0.3s;
}
}
.text {
.title {
padding: 5px;
}
}
.tree_son {
height: 0px;
overflow: hidden;
transition: all 0.3s;
}
- 类型声明
使用到的类型声明文件
export interface TreeProps {
/**
* @description 列表数据
* @default []
*/
treeData?: any;
/**
* @description 点击触发事件
*/
handleChange?: Function;
// /**
// * @description 数控组件类型,不同类型不同样式 可选 button text border
// * @default text
// */
// type?: string
left?: any;
a?: any
}
总结
实现树形控件主要就是一个函数组件的一个递归实现,先规定树形数据结构,通过数据结构实现组件递归,判断递归的结束条件,当没有children字段时结束递归,剩下就是一个样式的写法,怎么控制样式显示隐藏实现交互效果,我这个实现效果不是太好,局限性较差,过渡效果不是太好,我是通过超出隐藏和控制高度实现过渡效果。实现并不难,实现方法有很多,我这个写的不是太好需要优化的地方有很多。