React-View-UI组件库封装——Tree选择器

简介: React-View-UI组件库封装——Tree选择器组件封装记录

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

Tree选择器结构比较特殊,类似于数据结构中的树,因此设计对于优化有很多的关系。

先看一下组件库文档
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
设计中主要用到的思路是递归,先看一下基础渲染吧:

const treeData = [
  {
   
   
    title: 'parent1',
    value: '0-0',
    children: [
      {
   
   
        title: 'parent 1-0',
        value: '0-0-1',
      },
      {
   
   
        title: 'parent 1-1',
        value: '0-0-2',
        children: [
          {
   
   
            title: 'leaf2',
            value: '0-0-0-1',
          },
        ],
      },
    ],
  },
  {
   
   
    title: 'parent2',
    value: '0-1',
    children: [
      {
   
   
        title: 'parent 2-0',
        value: '0-0-3',
      },
    ],
  },
];

渲染结构是这样的,就是一个树的结构,通过渲染函数将所有树节点递归渲染出来,核心代码如下:
在这里插入图片描述
在这里插入图片描述
而具体这个level是从哪里来的呢,其实使用者上文只需要传递title、value、children即可,设计中其实是在渲染之前对这个树节点进行了一些结构改造的,以便于组件开发。
在这里插入图片描述
在上图中,二次改造函数对每个节点都进行了height和level的计算和添加,这些后面都会用到,具体备注在图片中很清楚。

业务开发
核心点主要在切换菜单,切换菜单时我的设计是展开只进行下一层节点的展示;收起的话如果是对根节点进行收起操作,则将所有子节点收起,核心代码如下:
在这里插入图片描述
上面所讲的是切换的实现,如果是点击无子节点的节点呢?就是直接选中操作了。
在这里插入图片描述
这里选中分为了单选了多选,组件默认是单选的,如需要支持多选,需要给组件传递avaChooseMore属性,具体可参照文档案例。

组件完整源码index.tsx:

import React, {
   
    FC, memo, Fragment, useState, useEffect, useCallback } from 'react';
import {
   
    CaretRightOutlined, CaretDownOutlined } from '@ant-design/icons';
import Input from '../Input';
import './index.module.less';

interface treeProps {
   
   
  /**
   * @description Tree配置参数
   */
  treeData: Array<treeNode>;
  /**
   * @description 宽度
   * @default 200px
   */
  width?: string;
  /**
   * @description 支持搜索
   * @default false
   */
  avaSearch?: boolean;
  /**
   * @description 支持多选
   * @default false
   */
  avaChooseMore?: boolean;
  /**
   * @description 全展开
   * @default false
   */
  defaultOpen?: boolean;
  /**
   * @description 选择回调函数
   */
  chooseCallback?: Function;
}
interface treeNode {
   
   
  title: string;
  value: string;
  level: number;
  height?: string;
  children?: Array<treeNode>;
}

const Tree: FC<treeProps> = (props) => {
   
   
  const {
   
    width = '200', treeData, avaSearch, avaChooseMore, defaultOpen, chooseCallback } = props;

  const [stateTreeData, setStateTreeData] = useState<Array<treeNode>>(treeData); //树结构
  const [activedVal, setActivedVal] = useState<string>(''); //选中的节点值
  const [containerHeight, setContainerHeight] = useState<string>('0px'); //容器高度
  const [isFocus, setIsFocus] = useState(false); //聚焦状态

  useEffect(() => {
   
   
    resolveTreeData(treeData as Array<treeNode>, 1);
    window.addEventListener('click', () => setContainerHeight('0px'));
  }, []);

  const resolveTreeData = (treeData: Array<treeNode>, nowIndexLevel: number) => {
   
   
    //二次处理treeData
    treeData.forEach((treeNode: treeNode) => {
   
   
      treeNode.level = nowIndexLevel;
      if (defaultOpen) {
   
   
        //默认全展开
        treeNode.height = '30px';
      } else {
   
   
        treeNode.height = treeNode.level == 1 ? '30px' : '0';
      }
      if (treeNode?.children?.length) {
   
   
        //有子节点
        resolveTreeData(treeNode.children, nowIndexLevel + 1);
      } else {
   
   
        //没有子节点,重置level为当前层级,继续寻找
        nowIndexLevel = treeNode.level;
      }
    });
    setStateTreeData(treeData); //更新状态
  };
  const toggleTreeMenu = (clickTreeNode: treeNode) => {
   
   
    //菜单切换或直接选中终极节点
    if (clickTreeNode?.children?.length) {
   
   
      //菜单切换的情况
      const oldStateTree = [...stateTreeData];
      const editTreeNode = (treeNode: Array<treeNode>) => {
   
   
        //所选节点后代收起处理函数
        treeNode.forEach((child) => {
   
   
          //找到节点,对子节点进行处理
          if (child?.children?.length) {
   
   
            child.height = '0';
            editTreeNode(child.children);
          } else {
   
   
            child.height = '0';
          }
        });
      };
      const mapFn = (treeNode: Array<treeNode>) => {
   
   
        //深度优先查找节点函数
        treeNode.forEach((t: treeNode, i: number) => {
   
   
          if (t.title == clickTreeNode.title && t.value == t.value) {
   
   
            if (t?.children?.length) {
   
   
              //后代节点处理,如果打开,只需打开下一代即可,如果关闭,需要关闭所有后代
              if (t.children[0].height == '0') {
   
   
                //打开
                t.children = t.children.map((child: treeNode) => {
   
   
                  return {
   
   
                    ...child,
                    height: child.height == '0' ? '30px' : '0',
                  };
                });
              } else {
   
   
                //关闭
                editTreeNode(t.children); //对后代节点进行处理
              }
            }
          } else if (t?.children?.length) {
   
   
            mapFn(t.children);
          }
        });
      };
      mapFn(oldStateTree);
      setStateTreeData(oldStateTree);
    } else {
   
   
      //选中终极节点的情况
      if (avaChooseMore) {
   
   
        //多选
        if (activedVal.split(',').includes(clickTreeNode.title)) {
   
   
          //取消选中
          let updateVal: Array<string> | string = activedVal;
          updateVal = updateVal.split(',');
          updateVal.splice(
            activedVal.split(',').findIndex((t) => t == clickTreeNode.title),
            1,
          );
          updateVal = updateVal.join(',');
          setActivedVal(updateVal);
          chooseCallback && chooseCallback(updateVal);
        } else {
   
   
          setActivedVal(
            activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
          );
          chooseCallback &&
            chooseCallback(
              activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
            );
        }
      } else {
   
   
        //单选
        setActivedVal(clickTreeNode.title);
        chooseCallback && chooseCallback(clickTreeNode.title);
      }
    }
  };
  const handleIptChange = (val: string) => {
   
   
    //文本改变回调
    if (avaSearch) {
   
   
      setActivedVal(val);
    } else {
   
   
      setActivedVal('');
    }
  };
  const handleClick = () => {
   
   
    //点击回调
    if (avaSearch) {
   
   
      if (isFocus && containerHeight == '100%') {
   
   
        setContainerHeight('0px');
      } else {
   
   
        setContainerHeight('100%');
      }
    } else {
   
   
      setContainerHeight(containerHeight == '0px' ? '100%' : '0px');
    }
  };
  const handleIptFocus = () => {
   
   
    //聚焦回调
    setTimeout(() => {
   
   
      //异步,等待点击执行完毕
      if (!isFocus) {
   
   
        setIsFocus(true);
      }
    }, 150);
  };
  const handleIptBlur = () => {
   
   
    //失去焦点回调
    setIsFocus(false);
  };
  const searchStyle = useCallback(
    (treeNode: treeNode): string => {
   
   
      //搜索高亮样式
      if (treeNode.title.includes(activedVal) && activedVal !== '') {
   
   
        return '#1890FF';
      } else {
   
   
        return '#000000';
      }
    },
    [activedVal],
  );
  const activedStyle = useCallback(
    (treeNode: treeNode): string => {
   
   
      //选中高亮样式
      if (avaChooseMore) {
   
   
        //多选
        if (activedVal.split(',').includes(treeNode.title)) {
   
   
          return '#bae8ff';
        } else {
   
   
          return '#ffffff';
        }
      } else {
   
   
        //单选
        if (activedVal == treeNode.title) {
   
   
          return '#bae8ff';
        } else {
   
   
          return '#ffffff';
        }
      }
    },
    [activedVal],
  );
  const clearCallback = () => {
   
   
    //清空
    setActivedVal('');
  };
  const render = (data: Array<treeNode> = stateTreeData) => {
   
   
    //动态规划render函数
    return data.map((treeNode: treeNode, index) => {
   
   
      return (
        <Fragment key={
   
   index}>
          <div
            className="treeNode"
            style={
   
   {
   
   
              marginLeft: `${
     
     treeNode.level * 10}px`,
              height: `${
     
     treeNode.height}`,
              color: searchStyle(treeNode),
              background: activedStyle(treeNode),
            }}
            onClick={
   
   () => toggleTreeMenu(treeNode)}
          >
            {
   
   
              treeNode?.children?.length ? (
                treeNode.children[0].height == '0' ? (
                  <CaretDownOutlined />
                ) : (
                  <CaretRightOutlined />
                )
              ) : (
                <div style={
   
   {
   
    width: '12px', height: '12px' }}></div>
              ) //空间占位符
            }

            <span className="text">{
   
   treeNode.title}</span>
          </div>
          {
   
   treeNode?.children?.length && render(treeNode.children)}
        </Fragment>
      );
    });
  };

  return (
    <Fragment>
      <div className="tree-container" onClick={
   
   (e) => e.stopPropagation()}>
        <Input
          moreStyle={
   
   avaSearch ? {
   
   } : {
   
    caretColor: 'transparent' }}
          placeholder={
   
   avaSearch ? '请输入' : ''}
          width={
   
   width}
          defaultValue={
   
   activedVal}
          handleClick={
   
   handleClick}
          handleIptChange={
   
   handleIptChange}
          handleIptFocus={
   
   handleIptFocus}
          handleIptBlur={
   
   handleIptBlur}
          clearCallback={
   
   clearCallback}
          showClear
        />

        <div
          className="tree-select-dialog"
          style={
   
   {
   
   
            width: `${
     
     width}px`,
            height: containerHeight,
            opacity: containerHeight == '0px' ? '0' : '1',
            padding: containerHeight == '0px' ? '0 10px 0 10px' : '10px',
          }}
        >
          {
   
   render()}
        </div>
      </div>
    </Fragment>
  );
};

export default memo(Tree);

github path戳~
React-View-UI组件库文档地址戳~

如果对该组件库有兴趣,在github的readme中有介绍基本入门方式,可以下载尝试,最后感谢花时间阅读此文,如果对Tree组件设计有更好的建议欢迎留言呀。

目录
相关文章
|
7月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
130 0
|
3月前
|
前端开发 JavaScript 网络架构
react对antd中Select组件二次封装
本文介绍了如何在React中对Ant Design(antd)的Select组件进行二次封装,包括创建MSelect组件、定义默认属性、渲染Select组件,并展示了如何使用Less进行样式定义和如何在项目中使用封装后的Select组件。
120 2
react对antd中Select组件二次封装
|
18天前
|
存储
「Mac畅玩鸿蒙与硬件34」UI互动应用篇11 - 颜色选择器
本篇将带你实现一个颜色选择器应用。用户可以从预设颜色中选择,或者通过输入颜色代码自定义颜色来动态更改界面背景。该应用展示了如何结合用户输入、状态管理和界面动态更新的功能。
34 3
「Mac畅玩鸿蒙与硬件34」UI互动应用篇11 - 颜色选择器
|
24天前
|
UED
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
本篇将带你实现一个滑动选择器应用,用户可以通过滑动条选择不同的数值,并实时查看选定的值和提示。这是一个学习如何使用 Slider 组件、状态管理和动态文本更新的良好实践。
34 1
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
|
17天前
|
前端开发 JavaScript 测试技术
React 时间选择器 Time Picker:常见问题与调试指南
本文介绍了在使用 React 时间选择器时常见的问题及解决方案,包括时间格式不匹配、时区问题、禁用时间范围和自定义样式等。通过代码案例详细解释了如何避免这些问题,强调了阅读文档、类型检查、单元测试和调试技巧的重要性。
33 7
|
16天前
|
前端开发 UED 开发者
React 日期时间选择器 (DateTime Picker): 从基础到高级
本文详细介绍了如何在React应用中集成日期时间选择器,重点讲解了`react-datepicker`和Material-UI的`DatePicker`组件的安装、基本用法、自定义日期格式和设置日期范围的方法。同时,文章还探讨了常见问题及其解决方法,帮助开发者避免易错点,确保在项目中顺利集成日期时间选择功能。
52 3
|
3月前
|
前端开发
React添加路径别名alias、接受props默认值、并二次封装antd中Modal组件与使用
本文介绍了在React项目中如何添加路径别名alias以简化模块引入路径,设置组件props的默认值,以及如何二次封装Ant Design的Modal组件。文章还提供了具体的代码示例,包括配置Webpack的alias、设置defaultProps以及封装Modal组件的步骤和方法。
86 1
React添加路径别名alias、接受props默认值、并二次封装antd中Modal组件与使用
|
2月前
|
前端开发
react 封装防抖
react 封装防抖
35 4
|
3月前
封装react-antd-table组件参数以及方法如rowSelection、pageNum、pageSize、分页方法等等
文章介绍了如何封装React-Antd的Table组件,包括参数和方法,如行选择(rowSelection)、页码(pageNum)、页面大小(pageSize)、分页方法等,以简化在不同表格组件中的重复代码。
77 0
|
5月前
element UI 组件封装--搜索表单(含插槽和内嵌组件)
element UI 组件封装--搜索表单(含插槽和内嵌组件)
156 5