一、自定义节点
使用ant design中的Tree组件,基础树形组件只需要将treeData属性绑定一个树形结构的值(treeData={treeData})即可:
但是由于单纯的展示名称已经不能满足这里的需求,使用将treeData处理后的TreeNode加到Tree的内部,代码如下,其中onSelect在点击树节点时触发:
// 点击节点,第一次点击节点是选中,第二次点击同一个节点是取消选中,用keys来判断是否有选中 const onSelect = (keys, info) => { if (keys.length > 0) { setSelectNode(info.node); } else { setSelectNode({}); } }; //...{ marginTop: "20px" }} showLine={false} showIcon={true} onSelect={onSelect} > {handleTreeData(treeData)}
获取treeData,笔者这里treeData的格式为:
[{ id:'1', name:'所有', count:'21', suffix:'江苏', childNodes:[ { id:'1-1', name:'南京', count:'21', suffix:'', childNodes:[] } ] }]
从接口获取:
const _getTreeData= async () => { setTreeData([]); try { let result = await getTreeData(); setTreeData(result); } catch (error) { } };
下面笔者需要处理数据,由上面的代码可知,通过handleTreeData函数处理数据treeData。分析一下基础的树形结构,Tree的节点其实是TreeNode,对于每一节点treeNode,根据官网的介绍,只需要设置其title和key属性,一般key属性即为树形数据的节点id,由此递归出所有的TreeNode:
// 重写树 const handleTreeData = (treeData) => { return treeData?.map((treeNode) => handleNodeData(treeNode)); }; const handleNodeData = (treeNode) => { if (treeNode.toString() === "[object Object]") { treeNode.title = ( {treeNode.name} ({treeNode.other.count}) _{treeNode.suffix} ); return ({treeNode?.childNodes?.map((n) => handleNodeData(n))} ); } return; };
至此,该树已经按照笔者的需求展示:
二、重命名节点
给每个节点后面添加一个按钮,点击按钮将节点切换为编辑状态,默认是原节点名称,根据上文,很容易想到在handleNodeData()中在treeNode.title中添加编辑按钮,并绑定rename():
此外,需要给每个节点增加isEdit和defaultValue(用于取消重命名后使用原来的节点名称)的属性,isEdit为true表示编辑态,否则正常展示节点。初始化数据,将所有节点的isEdit全部置为false。defaultValue值为name的值。
// 设置不可编辑 const setAllNotEdit = (arr) => { let data = [].concat(arr); data.forEach((val) => { val.isEdit = false; if (val.childNodes && val.childNodes.length) { setAllNotEdit(val.childNodes); } }); return data; }; // 查询组织树 const _getTreeData= async () => { //... let data = setAllNotEdit(result); setTreeData(data); //... };
点击重命名,触发rename():在树形数据中找到该节点数据(定义deepTree函数,用于找到目标节点数据,并将名为第三个入参的键的值修改为第四个入参),将isEdit改为true。在handleNodeData中针对编辑态和常规态作出单独的节点定义:
const deepTree = (arr, key, keyName, value, otherValue) => { let data = [].concat(arr); for (let i = 0; i < data.length; i++) { if (data[i].id === key) { data[i][keyName] = value; } else if (typeof otherValue === "boolean") { data[i][keyName] = otherValue; } if (data[i].childNodes && data[i].childNodes.length) { deepTree(data[i].childNodes, key, keyName, value, otherValue); } } return data; }; // 重命名 const rename = () => { if (selectNode && selectNode.key) { let data = deepTree(treeData, selectNode.key, "isEdit", true, false); setTreeData(data); } else { message.warning("请选择节点"); } }; //... const handleNodeData = (treeNode) => { if (treeNode.toString() === "[object Object]") { if (treeNode.isEdit) { treeNode.title = ( { changeNodeName(e,treeNode.id); }}/> ({treeNode.count}) _{treeNode.suffix} ); } else { //... } //... } return; };
到这里,当点击重命名按钮时,节点已经变为编辑态了,input后有确定和取消两个按钮。当在input中输入新名称,触发changeNodeName(),如果没有这一步,input的值将无法修改(因为始终绑定的是节点名称,节点名称没有改变过):
// 修改节点名称 const changeNodeName = (e, key) => { let data = deepTree(treeData, key, "name", e.target.value); setTreeData(data); };
点击取消时,表示取消重命名,使用原来的名称。在节点数据中找到当前节点,将值修改为之前的值(这个值我们已经保存在defaultValue中了):
// 取消修改节点名称 const cancelRename = (treeNode) => { let dataHasReset = deepTree( treeData, treeNode.id, "name", treeNode.defaultValue ); let data = setAllNotEdit(dataHasReset); setTreeData(data); };
点击确定时,表示修改节点名。
- 如果此时只需要页面的更新,那么只需要在节点数据中找到该节点,更新defaultValue:
const saveTreeNode = (treeNode) => { let dataHasChangeDefaultVal = deepTree( treeData, treeNode.id, "defaultValue", treeNode.name ); let data = setAllNotEdit(dataHasChangeDefaultVal); setTreeData(data); };
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
// 保存修改的节点名称 const saveTreeNode = async (treeNode) => { try { await updateNode({ //... }); _getTreeData(); } catch (e) { } };
在Tree控件中,点击一次节点,表示选中当前节点,再次点击,表示取消选中,但是当切换为编辑态的时候,我们可能多次点击,为了防止数据丢失,修改onSelect如下(其中dataref是TreeNode的props):
// 点击节点 const onSelect = (keys, info) => { if (keys.length > 0 || info.node?.dataRef?.isEdit) { setSelectNode(info.node); } else { setSelectNode({}); } }; const formatNodeData = (treeNode) => { if (treeNode.toString() === "[object Object]") { //... return (
{treeNode?.childNodes?.map((d) => formatNodeData(d))} ); } return; }) 在对选中的节点进行重命名之后,虽然树是新的树了,但是保存了节点选中的状态,也保存了被选的节点数据,为了防止数据不同步造成的误会,这里笔者每次得到新的树时,就会把选中的状态去掉,选中的数据也置空。选中的数据置空只需要setSelectNode({})即可。但是去掉选中的状态就要求使用Tree控件的另一个属性:selectedKeys,表示选中的节点,当加上这个属性,当点击节点后,需要将绑定的值也更新:
// 点击节点 const onSelect = (keys, info) => { setSelectedKeys(keys); //... }; //...
{formatTreeData(treeData)} 至此,重命名节点名称已经实现。
三、新增节点
笔者这里添加的是子节点,兄弟节点也类似,不再赘述。
所谓新增节点,其实就是处理树形结构的数据。
- 如果此时只需要页面的更新,在当前选中节点的childNodes中增加一个对象,所以,递归找到选中的节点,push一个新节点即可:
const onAdd = (arr) => { let data = [].concat(arr); data.forEach((item) => { if (item.id === selectNode.key) { if (!item.childNodes) { item.childNodes = []; } item.childNodes.push({ name: "新节点", defaultValue: "新节点", id: selectNode.key + Math.random(100), suffix:'', count:'', isEditable: false, childNodes: [], }); return; } if (item.childNodes) { onAdd(item.childNodes); } }); return data; }; const addNode = () => { if (selectNode && selectNode.key) { let data = onAdd(treeData); setTreeData(data); } else { message.warning("请选择节点"); } };
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
// 添加下级 const addNode = async () => { if (selectNode && selectNode.key) { try { let result = await addNode({ //... }); _getTreeData(); } catch (e) { } } };
至此,新增节点已经实现。
四、删除节点
与新增节点相对,删除节点是在数据中找到选中节点,从childNodes中删除元素。同样从两种场景出发:
- 如果此时只需要页面的更新,在当前选中节点的childNodes中删除一个对象,所以,递归找到选中的节点,splice即可:
const onDelete = (arr) => { arr.forEach((item, index) => { if (item.id === selectNode.key) { arr.splice(index, 1); return; } if (item.childNodes) { onDelete(item.childNodes); } }); return arr; }; const delNode = () => { if (selectNode && selectNode.key) { let data = onDelete(treeData); setTreeData([].concat(data)); setSelectNode({}); } else { message.warning("请选择节点"); } };
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
// 删除节点 const delNode = () => { if (selectNode && selectNode.key) { try { let result = await deleteNode({ //... }); _getTreeData(); } catch (e) {} } else {} };
至此,删除节点已经实现。
总结
本篇详述了对于react + ant design的树形控件的自定义节点,以及对节点的增删改,如有建议,欢迎指教~
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
- 如果此时只需要页面的更新,在当前选中节点的childNodes中删除一个对象,所以,递归找到选中的节点,splice即可:
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
- 如果此时只需要页面的更新,在当前选中节点的childNodes中增加一个对象,所以,递归找到选中的节点,push一个新节点即可:
- 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树: