详述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({});
    }
  };

//...

      <Tree
        style={{ marginTop: "20px" }}
        showLine={false}
        showIcon={true}
        onSelect={onSelect}
      >
        {handleTreeData(treeData)}
      </Tree>

        获取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 = (
          <div>
            <span className="text-overflow" title={treeNode.name}>{treeNode.name}</span>
            <span>({treeNode.other.count}) _{treeNode.suffix}</span>
          </div>
        );

        return (
            <TreeNode title={treeNode.title} key={treeNode.id}>
                {treeNode?.childNodes?.map((n) => handleNodeData(n))}
            </TreeNode>
        );
    }
    return <TreeNode {...treeNode} />;
  };

        至此,该树已经按照笔者的需求展示:


 二、重命名节点

        给每个节点后面添加一个按钮,点击按钮将节点切换为编辑状态,默认是原节点名称,根据上文,很容易想到在handleNodeData()中在treeNode.title中添加编辑按钮,并绑定rename():

<Button onClick={() => rename()} size={"small"}>重命名</Button>

        此外,需要给每个节点增加isEditdefaultValue(用于取消重命名后使用原来的节点名称)的属性,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 = (
          <div>
            <input value={treeNode.name} onChange={(e) => { changeNodeName(e,treeNode.id); }}/>
            ({treeNode.count})
            _{treeNode.suffix}
            <Button onClick={() => { saveTreeNode(treeNode); }} size={"small"} type="link" >确定</Button>
            <Button onClick={() => { cancelRename(treeNode); }} size={"small"} type="link" >取消</Button>
          </div>
        );
      } else {
            //...
        }
        //...
    }
    return <TreeNode {...treeNode} />;
};

        到这里,当点击重命名按钮时,节点已经变为编辑态了,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 title={treeNode.title} key={treeNode.id} dataRef={treeNode}>
          {treeNode?.childNodes?.map((d) => formatNodeData(d))}
        </TreeNode>
      );
    }
    return <TreeNode {...treeData} />;
})

        在对选中的节点进行重命名之后,虽然树是新的树了,但是保存了节点选中的状态,也保存了被选的节点数据,为了防止数据不同步造成的误会,这里笔者每次得到新的树时,就会把选中的状态去掉,选中的数据也置空。选中的数据置空只需要setSelectNode({})即可。但是去掉选中的状态就要求使用Tree控件的另一个属性:selectedKeys,表示选中的节点,当加上这个属性,当点击节点后,需要将绑定的值也更新:

  // 点击节点
  const onSelect = (keys, info) => {
    setSelectedKeys(keys);
    //...
  };

//...
      <Tree
        showLine={false}
        showIcon={true}
        defaultExpandAll={true}
        onSelect={onSelect}
        selectedKeys={selectedKeys}
      >
        {formatTreeData(treeData)}
      </Tree>

        至此,重命名节点名称已经实现。


三、新增节点

        笔者这里添加的是子节点,兄弟节点也类似,不再赘述。

        所谓新增节点,其实就是处理树形结构的数据。

  • 如果此时只需要页面的更新,在当前选中节点的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的树形控件的自定义节点,以及对节点的增删改,如有建议,欢迎指教~

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,针对您的问题,我可以给出以下的解答: 首先,在使用Ant Design的Tree组件时,我们可以通过`filterTreeNode`属性对节点进行筛选,该属性接收一个函数作为参数,该函数的返回值为布尔类型,用于决定该节点是否被筛选出来。 接下来,我们可以使用React的函数组件来创建可搜索的形控件。在函数组件中,我们可以通过useState钩子函数来维护搜索框的值,以及使用useMemo钩子函数来缓存筛选后的节点数据。在筛选函数中,我们可以使用字符串的includes方法来进行模糊匹配,如果搜索框的值含有节点的名称,则该节点被筛选出来。 最后,我们可以使用TypeScript来为组件添加类型检查,从而提高代码的可维护性和可读性。 下面是一份示例代码,仅供参考: ```typescript import React, { useState, useMemo } from 'react'; import { Tree } from 'antd'; interface TreeNode { title: string; key: string; children?: TreeNode[]; } const data: TreeNode[] = [ { title: 'Parent 1', key: '0-0', children: [ { title: 'Child 1', key: '0-0-0', }, { title: 'Child 2', key: '0-0-1', }, ], }, { title: 'Parent 2', key: '0-1', children: [ { title: 'Child 3', key: '0-1-0', }, { title: 'Child 4', key: '0-1-1', }, ], }, ]; const SearchableTree: React.FC = () => { const [searchValue, setSearchValue] = useState<string>(''); const filteredData = useMemo(() => filterTreeData(data, searchValue), [searchValue]); const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => { setSearchValue(e.target.value); }; return ( <div> <input type="text" value={searchValue} onChange={handleSearch} /> <Tree filterTreeNode={filterTreeNode}> {renderTreeNodes(filteredData)} </Tree> </div> ); }; const filterTreeData = (treeData: TreeNode[], searchValue: string): TreeNode[] => { if (!searchValue) { return treeData; } return treeData.reduce<TreeNode[]>((acc, node) => { const children = node.children && filterTreeData(node.children, searchValue); if (node.title.toLowerCase().includes(searchValue.toLowerCase()) || children?.length) { acc.push({ ...node, children, }); } return acc; }, []); }; const filterTreeNode = (node: TreeNode): boolean => { return !!node.children || node.title.toLowerCase().includes(searchValue.toLowerCase()); }; const renderTreeNodes = (treeData: TreeNode[]): React.ReactNode => { return treeData.map((node) => { if (node.children) { return ( <Tree.TreeNode key={node.key} title={node.title}> {renderTreeNodes(node.children)} </Tree.TreeNode> ); } return <Tree.TreeNode key={node.key} title={node.title} />; }); }; export default SearchableTree; ``` 希望能够对您有所帮助,如有疑问请随时追问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值