前言
之前遇到一个需求,可对于任意节点添加或删除子节点。首先技术栈是基于react+ant design,ant提供了Tree组件,但都是根据固定的数据渲染出树结构,如果需要新增或删除节点,官网并未提供。
实现过程
新增节点
首先,要记录选中节点,在有选中的情况下点击全局的新增按钮,就相当于在选中的节点下新增子节点,否则直接在最外层节点添加新的节点(此时的情况就是有多个并列的根节点)。当然也可以直接点击节点出现下拉菜单,选择操作
然后,实现新增功能,在点击新增按钮之后,相应的节点位置出现输入框,按回车或者输入框失去焦点代表输入完成。找到插入位置,将新增的节点插入。
输入状态:
输入完成后:
需要自定义节点,点击节点(ant Dropdown组件也支持右键)显示下拉弹窗。
这里的DropdownInput是自定义的组件,因为需要校验输入内容
// DropdownInput组件
import { Dropdown, Input } from "antd";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import type { InputProps } from "antd";
import _ from "lodash";
interface DropdownInputType extends InputProps {
errorInfo?: string;
initValue?: string;
}
const DropdownInputFun: React.ForwardRefRenderFunction<
unknown,
DropdownInputType
> = (props, ref) => {
const { errorInfo, initValue, onChange, onBlur, onPressEnter } = props;
const [open, setOpen] = useState<boolean>(false);
const [errorText, setErrorText] = useState<string>("请输入中英文数字及下划线");
const [value, setValue] = useState<string>(""); // 值
const inputRef = useRef<any>(null);
useImperativeHandle(ref, () => inputRef?.current);
useEffect(() => {
if (initValue) setValue(initValue);
}, [initValue]);
useEffect(() => {
if (errorInfo) setErrorText(errorInfo);
}, [errorInfo]);
/** 监听输入报错 */
const handleChange = _.debounce((e: any, isSure = false) => {
const { value } = e?.target;
const reg = /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/;
if (!reg.test(value)) {
setOpen(true);
} else {
setOpen(false);
onChange?.(value);
}
}, 300);
return (
<Dropdown
overlay={
<div
style={{
background: "#fff",
padding: "8px 12px",
height: 20,
boxShadow: "0px 2px 12px 0px rgba(0,0,0,0.06)",
}}
>
{errorText}
</div>
}
open={open}
>
<Input
ref={inputRef}
value={value}
onChange={(e) => {
e?.persist();
setValue(e?.target?.value);
handleChange(e);
}}
onBlur={(e) => {
!open && onBlur?.(e);
}}
onPressEnter={(e: any) => {
!open && onPressEnter?.(e);
}}
style={{ width: 272, borderColor: open ? "red" : "" }}
/>
</Dropdown>
);
};
const DropdownInput = forwardRef(DropdownInputFun);
export default DropdownInput;
// 自定义节点
const titleRender = (node: any) => {
const { title, icon, key, isInput } = node;
const paddingLeft = 16 * (node.level - 1);
if (isInput)
return (
<DropdownInput
ref={refInput}
initValue={title}
onPressEnter={(e) => onEnter(e, node)}
onBlur={(e) => onEnter(e, node)}
/>
);
return (
<Dropdown overlay={() => (
<Menu
onClick={(e) => {
if (e?.key === "add") addItem(node);
if (e?.key === "edit") editItem(node);
if (e?.key === "del") {
const data = mergeChildrenToParent1(treeData, node?.key);
setTreeData(data); // 更新树 数据
}
}}
>
<Menu.Item key="del">刪除</Menu.Item>
<Menu.Item key="add">新增</Menu.Item>
<Menu.Item key="edit">编辑</Menu.Item>
</Menu>
)}
trigger={["click"]}>
<div
key={key}
style={{ paddingLeft, display: "flex" }}
className="titleRoot"
>
{icon}
<div>{title}</div>
</div>
</Dropdown>
);
};
添加节点的addItem函数
// 添加节点
const addItem = (node: any) => {
const len = _.isEmpty(node?.children) ? 0 : node?.children?.length;
// 插入节点isInput为true,渲染节点的判断条件
const newChild = _.isEmpty(node.children)
? [{ isInput: true, key: `${node?.key}-${len}` }]
: [
{
isInput: true,
key: `${node?.key}-${len}`,
},
...node.children,
];
const data = updateTreeData(treeData, node, newChild);
setTreeData(data);
const expands = expandedKeys?.includes(node?.key)
? expandedKeys
: [node?.key, ...expandedKeys];
setExpandedKeys(expands);
setIsAdd(true);
};
const updateTreeData = (tree: any, target: any, children: any) => {
return tree.map((node: any) => {
if (node.key === target.key) {
return { ...node, children };
} else if (node?.children) {
return {
...node,
children: updateTreeData(node?.children, target, children),
};
}
return node;
});
};
输入完成后的onEnter函数
// 监听添加节点的输入
const onEnter = (e: any, node: any) => {
const value = e?.target?.value;
setIsAdd(false);
if (!value) {
// 输入内容为空就回车,直接删除编辑框的节点
const dele = deleteNodeByKey(treeData, node?.key);
setTreeData(dele);
return;
}
// 有输入内容就跟新
const data = updateItem(treeData, node?.key, value);
setTreeData(data);
};
// deleteNodeByKey
// 根据key 找到要删除的节点
const deleteNodeByKey: any = (treeData: any, keyToDelete: string) => {
return _.map(treeData, (node) => {
if (node.key === keyToDelete) {
// 如果节点的key匹配要删除的key,则返回undefined,表示不包括该节点
return undefined;
} else if (node.children) {
// 如果节点有子节点,则递归处理子节点
return {
...node,
children: deleteNodeByKey(node.children, keyToDelete),
};
}
return node; // 其他情况下返回原始节点
}).filter(Boolean); // 过滤掉undefined的节点
};
// updateItem
// 根据key 找到正在输入的节点,将输入内容跟新到title(显示节点的名字),并删除之前的isInput属性
const updateItem: any = (tree: any, key: string, data: any) => {
return _.map(tree, (item: any) => {
if (item?.key === key) {
item.title = data;
return _.omit(item, "isInput");
} else if (item?.children) {
return { ...item, children: updateItem(item?.children, key, data) };
}
return item;
});
};
这样一个新增节点的功能就完成了。
编辑节点
有了上面的新增功能,编辑就简单多啦,在将节点替换成编辑框时,只需要带上节点的title为输入框的默认值
const editItem = (node: any) => {
const data = editTreeItem(treeData, node?.key);
setTreeData(data);
setIsAdd(true);
};
// 节点呈编辑状态
export const editTreeItem: any = (tree: any, key: string) => {
return _.map(tree, (item: any) => {
if (item?.key === key) {
item.isInput = true;
console.log("进来啦",item);
return item;
} else if (item?.children) {
return { ...item, children: editTreeItem(item?.children, key) };
}
return item;
});
};
后面的逻辑就和新增一样啦,监听输入框的回车和失焦事件,完成编辑功能。
删除节点
删除节点要考虑是否删除节点下的子节点,如果直接删除子节点,逻辑就简单了,如果需要把删除节点的子节点给删除节点父节点,需要额外处理
// 直接删除
const deleteNodeByKey: any = (treeData: any, keyToDelete: string) => {
return _.map(treeData, (node) => {
if (node.key === keyToDelete) {
// 如果节点的key匹配要删除的key,则返回undefined,表示不包括该节点
return undefined;
} else if (node.children) {
// 如果节点有子节点,则递归处理子节点
return {
...node,
children: deleteNodeByKey(node.children, keyToDelete),
};
}
return node; // 其他情况下返回原始节点
}).filter(Boolean); // 过滤掉undefined的节点
};
// 删除节点,子节点合并到上级
const mergeChildrenToParent: any = (
treeData: any,
keyToDelete: string
) => {
return _.flatMap(treeData, (node) => {
if (node.key === keyToDelete) {
// 如果节点的key匹配要删除的key
if (node.children) {
// 如果有子节点,将子节点合并到当前节点的父节点中
const parent = _.find(treeData, (parentNode) => {
return _.some(parentNode.children, { key: keyToDelete });
});
if (parent) {
parent.children = [
...(parent.children || []),
...(node.children || []),
];
}
return undefined; // 返回undefined,表示删除当前节点
} else {
return undefined; // 如果没有子节点,直接删除当前节点
}
} else if (node.children) {
// 如果节点有子节点,则递归处理子节点
return {
...node,
children: mergeChildrenToParent(node.children, keyToDelete),
};
}
return node; // 其他情况下返回原始节点
}).filter(Boolean); // 过滤掉undefined的节点
};
附上Tree组件。里面的函数,上面都有,就不一一写完成了
import React, { useEffect, useRef, useState } from "react";
import { Button, Dropdown, Menu, Tree } from "antd";
import { DownOutlined } from "@ant-design/icons";
import DropdownInput from "@/components/DropdownInput";
const DemoTree = () => {
const [visible, setVisible] = useState<boolean>(false);
const [treeData, setTreeData] = useState([
{
title: "根节点1",
key: "1-0",
children: [
{
title: "子节点1",
key: "1-0-0",
},
{
title: "子节点2",
key: "1-0-1",
},
{
title: "子节点3",
key: "1-0-2",
},
],
},
{
title: "根节点2",
key: "2-1",
children: [
{
title: "子节点4",
key: "2-1-0",
},
{
title: "子节点5",
key: "2-1-1",
},
],
},
{
title: "根节点3",
key: "3-1",
children: [
{
title: "子节点6",
key: "3-1-0",
children:[{
title:'jjj',
key:'dfv'
}]
},
{
title: "子节点7",
key: "3-1-1",
},
],
},
]);
const refInput = useRef<any>(null);
const [expandedKeys, setExpandedKeys] = useState<any[]>([]);
const editItem = (node: any) => {};
// 添加节点
const addItem = (node: any) => {};
// 监听添加节点的输入
const onEnter = (e: any, node: any) => {};
// 自定义节点
const titleRender = (node: any) => {
const { title, icon, key, isInput } = node;
const paddingLeft = 16 * (node.level - 1);
if (isInput)
return (
<DropdownInput
ref={refInput}
initValue={title}
onPressEnter={(e) => onEnter(e, node)}
onBlur={(e) => onEnter(e, node)}
/>
);
return (
<Dropdown overlay={() =>(
<Menu
onClick={(e) => {
if (e?.key === "add") addItem(node);
if (e?.key === "edit") editItem(node);
if (e?.key === "del") {
// 这里用的方法上面都有哦。这里没引入
const data = mergeChildrenToParent(treeData, node?.key);
setTreeData(data);
}
}}
>
<Menu.Item key="del">刪除</Menu.Item>
<Menu.Item key="add">新增</Menu.Item>
<Menu.Item key="edit">编辑</Menu.Item>
</Menu>
)} trigger={["click"]}>
<div
key={key}
style={{ paddingLeft, display: "flex" }}
className="titleRoot"
>
{icon}
<div>{title}</div>
</div>
</Dropdown>
);
};
return (
<div>
<Tree
treeData={treeData}
expandedKeys={expandedKeys}
switcherIcon={<DownOutlined />}
titleRender={titleRender}
onExpand={(keys: any[]) => setExpandedKeys(keys)}
/>
</div>
);
};
export default DemoTree;
本文仅供参考,个人观点。