当React遇到树形穿梭框咋办?

本文已参与好文召集令活动,点击查看: 后端、大前端双赛道投稿,2万元奖池等你挑战!

本篇文章其实是上篇文章React 5种高级组件模式结合
实际业务需求和同伴们使用需求 所完成的实践篇。本片文章我想阐述的观点是:招无定式,水无常形 上篇文章只是讲述了设计组件时的一些设计模式,设计原理,给大家在设计组件时提供一些思路。其实理论和实践还是存在差距的。什么是一个好的组件,我觉得好的组件就是:拿来就用, 我不需要去理解你的内部原理,你只要给我想要的就OK!所以本篇文章的组件代码是宗和了实际的业务需求和同伴们的使用需求下的产物! 它不是适合所有人都能用的组件!, 如果这篇文章的组件也解决了你的需求,请给我一个赞!创作不易,支持支持!

项目源代码-点击-梯子自备

会使用codesandbox同学点这里在线编辑

组件效果图

屏幕录制2021-06-30 13.gif

组件设计思路

在设计组件之前特意和产品详细沟通,这个树形组件两侧数据结构应该都是树形结构,另外还会有一个单独的树形选择框。在经过和UI的讨论确定了树形穿梭框和树形选择框的样式,经过和同事沟通他们使用组件时想要传递的Props和回调之后开始动手创作!所以才有了效果图那样的两个组件。

树形穿梭框原理图 如下:
未命名文件.png

树形选择框原理图 如下:

未命名文件 (1).png

拆分分析

我们将会对这个树形穿梭框进行拆分分析,分析每一个单独的小组件的作用和功能。也会附上相对应的代码。

左侧选择框

对左侧选择框来说,它是这个组件的核心之一,它不仅是树形穿梭框的左侧,也是单独的树形选择框组件,所以对它来说要具备以下功能:

  • 输入框筛选树形数据

核心代码如下:

/** src/treeShuttleBox/tool.ts */

const searchTree = (searchValue = "", treeArr: any[] = []) => {
  // 树形数据搜索
  let searTreeArr: any[] = [];
  treeArr?.forEach((treeItem: { title: string; children: any[] }) => {
    if (treeItem.title.includes(searchValue)) {
      searTreeArr.push(treeItem);
    } else {
      if (treeItem.children && treeItem.children.length) {
        const chr = searchTree(searchValue, treeItem.children);
        const obj = {
          ...treeItem,
          children: chr,
        };
        if (chr && chr.length) {
          searTreeArr.push(obj);
        }
      }
    }
  });
  return searTreeArr;
};
复制代码
/** src/treeShuttleBox/TreeShuttleBoxLeft.tsx */
const handleSearchTree = (value: string) => {
      if (value === "") {
        // 点击清除搜索或者手动删除文字按下回车
        const treeData = JSON.parse(
          JSON.stringify(readOnlyTreeDataSource.treeData)
        );
        // 处理左侧数据源选中数据的禁用
        traverseTreeData({
          treeData,
          callback(item) {
            item["disabled"] = finallyValue.includes(item.key);
          },
        });
        // 保存左侧的数据源
        handleDataSourceLeftChange({
          treeData,
        });
      } else {
        // 搜索树
        handleDataSourceLeftChange({
          treeData: searchTree(value, dataSourceLeft.treeData),
        });
      }
    };
复制代码
  • 组件数据源的监听

核心代码如下:

/** src/treeShuttleBox/TreeShuttleBoxLeft.tsx */
useEffect(() => {
    // 遍历树形数据设置parentId
    const sonParMap: Map<string, string | null> = new Map();
    traverseTreeData({
      treeData,
      callback(item, index, parentId) {
        item.parentId = parentId;
        sonParMap.set(item.key, item.parentId);
      },
    });
    handleSonParMapChange(sonParMap);
    // 将所以树形数据拉平,为后续组装右侧的树形树形使用
    const flatArray = flatTree({
      treeData: JSON.parse(JSON.stringify(treeData)),
    });
    handleFlatArrayChange(flatArray);
    // 得到左侧所有的key,主要用于全选
    const allKey: string[] = flatArray.map(({ key }) => key);
    // 保存左侧数据源所有的key
    handleReadOnlyAllKeyLeftChange(allKey);
    // 保存到左侧的数据源
    handleDataSourceLeftChange({
      treeData: JSON.parse(JSON.stringify(treeData)),
    });
    // 保存左侧树的数据源,只读,用于搜索
    handleReadOnlyTreeDataSourceChange({
      treeData: JSON.parse(JSON.stringify(treeData)),
    });
  }, [treeData]);
复制代码
useEffect(() => {
    // 左侧选择数据时回调change --> 当组件为树形选择框时的回调函数
    handleChange && handleChange(checkListLeft);
  }, [checkListLeft]);
复制代码
  • 全选和半选

核心代码如下:

/** src/treeShuttleBox/TreeShuttleBoxLeft.tsx */
const handleCheckBoxChange = (e: any) => {
      // 处理全选和半选
      handleCheckListLeftChange(e.target.checked ? readOnlyAllKeyLeft : []);
      // 处理全选状态
      setCheckAll(e.target.checked);
      // 处理半选状态
      handleIndeterminateLeftChange(false);
      // 处理to Right 高亮
      handleIsBrightRightChange(e.target.checked);
    };
    
 const handleSelectNode = (keyArr: any) => {
      // 处理左侧选中的key
      handleCheckListLeftChange(keyArr);
      // 处理全选状态
      setCheckAll(readOnlyAllKeyLeft.length === keyArr.length);
      // 处理半选状态
      handleIndeterminateLeftChange(
        keyArr.length && readOnlyAllKeyLeft.length !== keyArr.length
      );
      // 处理 to Right 高亮
      handleIsBrightRightChange(!!keyArr.length);
    };
复制代码

中间按钮

中间的两个按钮主要负责了移动树形数据,禁用已选择的节点,更新最终的值。

  • ToRight按钮

原理图:

未命名文件.png

核心代码如下:

/** src/treeShuttleBox/TreeShuttleButton.tsx */
const handleClickToRight = () => {
      if (checkListLeft.length === 0 && !isBrightRight) {
        return;
      }
      // 取消左侧的半选
      handleIndeterminateLeftChange(false);
      if (readOnlyAllKeyLeft.length === checkListLeft.length) {
        // 左侧全选逻辑
        // 右侧数据源保存
        handleDataSourceRightChange(
          JSON.parse(JSON.stringify(readOnlyTreeDataSource))
        );
        // 保存右侧只读的数据源所有的key
        handleReadOnlyAllKeyRightChange([...checkListLeft]);
        // 保存最终值的key
        handleFinallyValueChange([...checkListLeft]);
        // 保存右侧只读的数据源用于搜索
        handleReadOnlyDataSourceRightChange({
          treeData: JSON.parse(JSON.stringify(checkListLeft)),
        });
        // 禁用左侧树已经选择的节点
        const treeData_ = JSON.parse(
          JSON.stringify(readOnlyTreeDataSource.treeData)
        );
        traverseTreeData({
          treeData: treeData_,
          callback(item) {
            item["disabled"] = true;
          },
        });
        handleDataSourceLeftChange({
          treeData: treeData_,
        });
      } else {
        // 左侧不全选逻辑
        // 得到子级的key 和 半选状态的 父级的key 用于重组数组
        let allKey = searchAllParents(checkListLeft, sonParMap);
        allKey = Array.from(new Set(allKey.concat(finallyValue)));
        // 得到选中的key 所对应的节点
        const selectNode = flatArray.filter(({ key }) => allKey.includes(key));
        // 重组树形数据结构
        const treeData = reorganizeTree({
          treeArr: selectNode,
        });
        // 右侧数据源保存
        handleDataSourceRightChange({ treeData });
        // 保存右侧只读的数据源所有的key
        handleReadOnlyAllKeyRightChange(allKey);
        // 保存最终值的key
        handleFinallyValueChange(allKey);
        // 保存右侧只读的数据源用于搜索
        handleReadOnlyDataSourceRightChange({
          treeData: JSON.parse(JSON.stringify(treeData)),
        });
        // 禁用左侧树已经选择的节点
        traverseTreeData({
          treeData: dataSourceLeft.treeData,
          callback(item) {
            item["disabled"] = allKey.includes(item.key);
          },
        });
        handleDataSourceLeftChange({
          treeData: [...dataSourceLeft.treeData],
        });
      }
      // 右侧按钮取消高亮
      handleIsBrightRightChange(false);
    };
复制代码
  • ToLeft按钮

原理图:

未命名文件 (2).png

核心代码如下:

/** src/treeShuttleBox/TreeShuttleButton.tsx */
const handleClickToLeft = () => {
      if (checkListRight.length === 0 && !isBrightLeft) {
        return;
      }

      if (checkListRight.length === readOnlyAllKeyRight.length) {
        // 右侧全选逻辑
        // 保存左侧选择的key
        handleCheckListLeftChange([]);
        // 保存右侧的数据源
        handleDataSourceRightChange({ treeData: [] });
        // 保存右侧所有的key
        handleReadOnlyAllKeyRightChange([]);
        // 更改最终的key值
        handleFinallyValueChange([]);
        // 更改右侧选中的key值
        handleCheckListRightChange([]);
        // 保存右侧只读的数据源
        handleReadOnlyDataSourceRightChange({
          treeData: [],
        });
        // 保存左侧的数据源
        handleDataSourceLeftChange(
          JSON.parse(JSON.stringify(readOnlyTreeDataSource))
        );
      } else {
        // 右侧不全选逻辑
        // 在最终值里删除右边勾选的key
        const finallyArr = finallyValue.filter(
          (key) => !checkListRight.includes(key)
        );
        // 筛选右侧的树形数据
        const selectNode = flatArray.filter(({ key }) =>
          finallyArr.includes(key)
        );
        // 重组右侧树形数据
        const treeData = reorganizeTree({
          treeArr: selectNode,
        });
        // 更新左侧选择的key
        const selectKeyLeft = checkListLeft.filter(
          (key) => !checkListRight.includes(key)
        );
        // 保存左侧选择的key
        handleCheckListLeftChange(selectKeyLeft);
        // 保存右侧的数据源
        handleDataSourceRightChange({ treeData });
        // 保存右侧所有的key
        handleReadOnlyAllKeyRightChange(finallyArr);
        // 更改最终的key值
        handleFinallyValueChange(finallyArr);
        // 更改右侧选中的key值
        handleCheckListRightChange([]);
        // 保存右侧只读的数据源
        handleReadOnlyDataSourceRightChange({
          treeData: JSON.parse(JSON.stringify(treeData)),
        });
        // 取消左侧树数据源的禁用
        traverseTreeData({
          treeData: dataSourceLeft.treeData,
          callback(item) {
            item["disabled"] = finallyArr.includes(item.key);
          },
        });
        // 保存左侧的数据源
        handleDataSourceLeftChange({
          treeData: JSON.parse(JSON.stringify(dataSourceLeft.treeData)),
        });
      }

      // 取消左侧高亮显示
      handleIsBrightLeftChange(false);
    };
复制代码

右侧选择框

对于右侧选择框来说还是比较简单,它只需要负责数据的展示和搜索就可以了。

  • 输入框筛选树形数据

对于筛选的核心代码其实都一样,这里可以直接查看左侧选择框的代码!

核心代码如下:

这段代码具有差异性!

/** src/treeShuttleBox/TreeShuttleBoxRight.tsx */
const handleSearchTree = (value: string) => {
      if (value === "") {
        // 点击清除搜索或者手动删除文字按下回车
        const treeData = JSON.parse(JSON.stringify(readOnlyDataSourceRight));
        // 保存右侧数据源
        handleDataSourceRightChange(treeData);
      } else {
        // 搜索树
        handleDataSourceRightChange({
          treeData: searchTree(value, dataSourceRight.treeData),
        });
      }
    };
复制代码
  • 全选和半选

这段代码和左侧选择框的代码一致,请看左侧选择框全选半选的代码!

总结

源码的链接我已经放到了文章的前面,感兴趣的同学可以看看。

总的来说,组件的设计还是得贴合实际的业务需求和同事的使用需求,我们只要掌握了组件的设计思路和一些高级的设计模式,我觉得一般的组件设计不在话下,这篇文章您要是觉得还不错,请不要吝啬您的赞,点一点赞!!
本片文章是基于Antd的树组件书写的,所以穿梭框也支持大数据的虚拟滚动,另外,本片文章中的源码也有可以优化的地方,比如,怎么才能使查找父级Key时更快一点,减少无意义的查询和递归。另外本片文章的源码我写了很详细的注释,如果还有不懂的地方欢迎留言。

往期精彩

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享