详述react + ant desgin自定义树的节点,对节点进行重命名新增删除等操作

一、自定义节点

        使用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的树形控件的自定义节点,以及对节点的增删改,如有建议,欢迎指教~