使用 React 和 MUI 创建多选 Checkbox 树组件

news2024/11/28 16:40:45

在本篇博客中,我们将使用 React 和 MUI(Material-UI)库来创建一个多选 Checkbox 树组件。该组件可以用于展示树形结构的数据,并允许用户选择多个节点。

前提

在开始之前,确保你已经安装了以下依赖:

  • React
  • MUI(Material-UI)

最终样式

非全选状态

在这里插入图片描述

全选状态

在这里插入图片描述

思路

我们的目标是创建一个多选 Checkbox 树组件,它可以接收树节点数据,并根据用户的选择返回选中的节点数据。为了实现这个目标,我们将按照以下步骤进行:

  1. 创建一个 React 函数组件 CheckBoxTree,它接收一个 data 属性作为树节点数据,并可选地接收一个 handleCheckData 属性作为回调函数,用于传递选中的节点数据。
  2. 在组件的状态中,创建一个 selected 数组,用于存储选中的节点的 id。
  3. 实现一个 onCheck 函数,用于处理节点 Checkbox 的点击事件。在该函数中,我们将根据用户的选择更新 selected 数组,并递归处理子节点的选中状态。
  4. 实现一个 renderTree 函数,用于递归渲染树节点。在该函数中,我们将根据节点的选中状态和子节点的数量来渲染 Checkbox 和节点名称。
  5. 使用 TreeViewTreeItem 组件来展示树形结构,并将树节点数据传递给 renderTree 函数进行渲染。

步骤

下面是实现多选 Checkbox 树组件的详细步骤:

1. 创建 React 函数组件

首先,我们需要创建一个 React 函数组件 CheckBoxTree,并定义它的属性和状态。代码如下:

import React from 'react';

interface CheckboxTreeState {
  selected: string[];
}

interface CheckBoxTreeProps {
  data: RegionType[]; //起码要包含childre,name和parentId,
  handleCheckData?: (data: string[]) => void;
}

export default function CheckBoxTree(props: CheckBoxTreeProps) {
  const { data, handleCheckData } = props;

  const [state, setState] = React.useState<CheckboxTreeState>({
    selected: []
  });

  // ...
}

2. 分割父节点

接下来,我们定义了splitNodeId函数,用于将节点id拆分为所有父节点id。它接受一个节点id字符串,格式为'1_2_3',并返回一个父节点id数组,例如['1_2', '1']。3表示的是当前节点。

/**
 * 拆分节点id为所有父节点id
 * @param id 节点id,格式为'1_2_3'
 * @returns 父节点id数组,如['1_2', '1']
 */
function splitNodeId(id: string) {
  // 按'_'分割节点id
  const path = id.split('_');

  // 累加生成父节点id
  return path.reduce((result: string[], current) => {
    // 拼接'_'和当前节点
    result.push(`${result.at(-1) ? result.at(-1) + '_' : ''}${current}`);
    return result;
  }, []);
}

3. 实现节点 Checkbox 的点击事件处理函数

接下来,我们需要实现一个 onCheck 函数,用于处理节点 Checkbox 的点击事件。在该函数中,我们将根据用户的选择更新 selected 数组,并递归处理子节点的选中状态。代码如下:

const onCheck = (
  event: React.ChangeEvent<HTMLInputElement>,
  node: RegionType,
  parentNodeName?: string
) => {
  const { checked } = event.target;
  const currentId = parentNodeName ?
    `${parentNodeName}_${node.id.id}` :
    node.id.id;
  const parentAreaName = splitNodeId(currentId);

  if (checked) {
    setState((prevState) => ({
      ...prevState,
      selected: Array.from(
        new Set([...prevState.selected, ...parentAreaName])
      )
    }));

    if (node.children && node.children.length > 0) {
      node.children.forEach((item) => {
        onCheck(event, item, currentId);
      });
    }
  } else if (!checked) {
    let tempState = { ...state };

    for (let index = parentAreaName.length - 1; index >= 0; index--) {
      const element = parentAreaName[index];

      if (
        tempState.selected.filter((id) => id.startsWith(`${element}_`))
          .length === 0
      ) {
        tempState = {
          ...tempState,
          selected: tempState.selected.filter((id) => id !== element)
        };
      }

      if (
        tempState.selected.filter((id) => id.startsWith(`${currentId}_`))
          .length !== 0
      ) {
        tempState = {
          ...tempState,
          selected: tempState.selected.filter(
            (id) =>
              !id.startsWith(`${currentId}_`) &&
              !id.startsWith(`${currentId}`)
          )
        };
      }
    }

    setState(tempState);
  }
};

4. 实现递归渲染树节点的函数

然后,我们需要实现一个 renderTree 函数,用于递归渲染树节点。在该函数中,我们将根据节点的选中状态和子节点的数量来渲染 Checkbox 和节点名称。代码如下:

const renderTree = (nodes: RegionType, parentNodeName?: string) => {
  let currentLength = 0;

  function getNodeLength(currentNodes: RegionType) {
    currentNodes.children?.forEach((node) => {
      currentLength++;
      if (node.children) {
        getNodeLength(node);
      }
    });
  }

  const currentId = parentNodeName ?
    `${parentNodeName}_${nodes.id.id}` :
    nodes.id.id;

  getNodeLength(nodes);

  return (
    <TreeItem
      key={nodes.id.id}
      nodeId={nodes.id.id}
      label={
        <FormControlLabel
          onClick={(e) => e.stopPropagation()}
          control={
            <Checkbox
              name={nodes.name}
              checked={
                nodes.children &&
                  nodes.children.length &&
                  state.selected.filter((id) =>
                    id.startsWith(`${currentId}_`)
                  ).length === currentLength ||
                state.selected.some((id) => id === currentId)
              }
              indeterminate={
                nodes.children &&
                nodes.children.length > 0 &&
                state.selected.some((id) => id.startsWith(`${currentId}_`)) &&
                state.selected.filter((id) => id.startsWith(`${currentId}_`))
                  .length < currentLength
              }
              onChange={(e) => {
                e.stopPropagation();
                onCheck(e, nodes, parentNodeName);
              }}
              onClick={(e) => e.stopPropagation()}
            />
          }
          label={nodes.name}
        />
      }
    >
      {Array.isArray(nodes.children) ?
        nodes.children.map((node) => renderTree(node, currentId)) :
        null}
    </TreeItem>
  );
};

5. 渲染树形结构

最后,我们使用 TreeViewTreeItem 组件来展示树形结构,并将树节点数据传递给 renderTree 函数进行渲染。代码如下:

return (
  <TreeView
    aria-label="checkbox tree"
    defaultCollapseIcon={<ExpandMore />}
    defaultExpandIcon={<ChevronRight />}
    disableSelection={true}
  >
    {data.map((item) => {
      return renderTree(item);
    })}
  </TreeView>
);

6. 完整代码

import { ChevronRight, ExpandMore } from '@mui/icons-material';
import { TreeItem, TreeView } from '@mui/lab';
import { Checkbox, FormControlLabel } from '@mui/material';
import React from 'react';

export interface RegionType {
  abbreviation: string;
  children?: RegionType[];
  createdTime: number;
  id: EntityData;
  level: number;
  name: string;
  nameCn: string;
  parentId: string;
  sort: number;
  status: boolean;
}

// 组件状态
int
erface CheckboxTreeState {
  // 选中节点id数组
  selected: string[];
}

// 组件属性
interface CheckBoxTreeProps {
  // 树节点数据
  data: RegionType[];
  // 向外传递选择框数据,
  handleCheckData?: (data: string[]) => void;
}

/**
 * 拆分节点id为所有父节点id
 * @param id 节点id,格式为'1_2_3'
 * @returns 父节点id数组,如['1_2', '1']
 */
function splitNodeId(id: string) {
  // 按'_'分割节点id
  const path = id.split('_');

  // 累加生成父节点id
  return path.reduce((result: string[], current) => {
    // 拼接'_'和当前节点
    result.push(`${result.at(-1) ? result.at(-1) + '_' : ''}${current}`);
    return result;
  }, []);
}

/**
 * 多选Checkbox树组件
 * @param props 组件属性
 * @returns JSX组件
 */
export default function CheckBoxTree(props: CheckBoxTreeProps) {
  // 获取树节点数据
  const { data, handleCheckData } = props;

  // 组件状态:选中节点id数组
  const [state, setState] = React.useState<CheckboxTreeState>({
    selected: []
  });

  /**
   * 点击节点Checkbox触发
   * @param event 事件对象
   * @param node 节点对象
   * @param parentNodeName 父节点名称
   */
  const onCheck = (
    event: React.ChangeEvent<HTMLInputElement>,
    node: RegionType,
    parentNodeName?: string
  ) => {
    // 获取Checkbox选中状态
    const { checked } = event.target;

    // 当前节点id
    const currentId = parentNodeName ?
      `${parentNodeName}_${node.id.id}` :
      node.id.id;

    // 父节点id数组
    const parentAreaName = splitNodeId(currentId);

    // 选中状态:选中当前节点和父节点
    if (checked) {
      setState((prevState) => ({
        ...prevState,
        //使用Set对selected数组去重
        selected: Array.from(
          new Set([...prevState.selected, ...parentAreaName])
        )
      }));

      // 若有子节点,递归选中
      if (node.children && node.children.length > 0) {
        node.children.forEach((item) => {
          onCheck(event, item, currentId);
        });
      }
    } else if (!checked) {
      // 临时state
      let tempState = { ...state };

      // 逆序遍历,进行选中状态更新
      for (let index = parentAreaName.length - 1; index >= 0; index--) {
        const element = parentAreaName[index];

        // 若父区域已无选中节点,取消选中父区域
        if (
          tempState.selected.filter((id) => id.startsWith(`${element}_`))
            .length === 0
        ) {
          tempState = {
            ...tempState,
            selected: tempState.selected.filter((id) => id !== element)
          };
        }

        // 取消选中当前区域
        if (
          tempState.selected.filter((id) => id.startsWith(`${currentId}_`))
            .length !== 0
        ) {
          tempState = {
            ...tempState,
            selected: tempState.selected.filter(
              (id) =>
                !id.startsWith(`${currentId}_`) &&
                !id.startsWith(`${currentId}`)
            )
          };
        }
      }
      // 更新state
      setState(tempState);
    }
  };

  /**
   * 递归渲染树节点
   * @param nodes 树节点数组
   * @param parentNodeName 父节点名称
   * @returns JSX组件
   */
  const renderTree = (nodes: RegionType, parentNodeName?: string) => {
    // 子节点总数
    let currentLength = 0;

    /**
     * 获取子节点总数
     * @param currentNodes 当前节点
     */
    function getNodeLength(currentNodes: RegionType) {
      currentNodes.children?.forEach((node) => {
        currentLength++;
        if (node.children) {
          getNodeLength(node);
        }
      });
    }

    // 当前节点id
    const currentId = parentNodeName ?
      `${parentNodeName}_${nodes.id.id}` :
      nodes.id.id;

    // 获取当前节点子节点总数
    getNodeLength(nodes);

    return (
      <TreeItem
        key={nodes.id.id}
        nodeId={nodes.id.id}
        sx={{
          '.MuiTreeItem-label': {
            'maxWidth': '100%',
            'overflow': 'hidden',
            'wordBreak': 'break-all',
            '.MuiFormControlLabel-label': {
              pt: '2px'
            }
          }
        }}
        label={
          <FormControlLabel
            onClick={(e) => e.stopPropagation()}
            sx={{ alignItems: 'flex-start', mt: 1 }}
            control={
              <Checkbox
                name={nodes.name}
                sx={{ pt: 0 }}
                checked={
                  // 若有子节点,判断子节点是否全部选中
                  // 或节点自身是否选中
                  nodes.children &&
                    nodes.children.length &&
                    state.selected.filter((id) =>
                      id.startsWith(`${currentId}_`)
                    ).length === currentLength ||
                  state.selected.some((id) => id === currentId)
                }
                indeterminate={
                  // 子节点存在选中与非选中状态
                  nodes.children &&
                  nodes.children.length > 0 &&
                  state.selected.some((id) => id.startsWith(`${currentId}_`)) &&
                  state.selected.filter((id) => id.startsWith(`${currentId}_`))
                    .length < currentLength
                }
                onChange={(e) => {
                  e.stopPropagation();
                  onCheck(e, nodes, parentNodeName);
                }}
                onClick={(e) => e.stopPropagation()}
              />
            }
            label={nodes.name}
          />
        }
      >
        {Array.isArray(nodes.children) ?
          nodes.children.map((node) => renderTree(node, currentId)) :
          null}
      </TreeItem>
    );
  };

  /**
   * 组件加载时触发,获取去重后的多选框id列表
   */
  React.useEffect(() => {
    // state.selected拆分数组并合并,返回成一个数组,如果需要去重后的值,可以使用Array.from(new set)
    const checkBoxList = state.selected.flatMap((item) => item.split('_'));
    // 因为是通过parent id来绑定子元素,所以下面的元素是只返回最后的子元素
    const checkTransferList = checkBoxList.filter(
      (value) => checkBoxList.indexOf(value) === checkBoxList.lastIndexOf(value)
    );

    // 从多选值数组中生成集合Set,再使用Array.from转换为数组
    if (handleCheckData) {
      handleCheckData(checkTransferList);
    }
  }, [state]);

  React.useEffect(() => {
    if (data.length) {
      setState({ selected: [] });
    }
  }, [data]);

  return (
    <TreeView
      aria-label="checkbox tree"
      defaultCollapseIcon={<ExpandMore />}
      defaultExpandIcon={<ChevronRight />}
      disableSelection={true}
    >
      {data.map((item) => {
        return renderTree(item);
      })}
    </TreeView>
  );
}

总结

通过以上步骤,我们成功地创建了一个多选 Checkbox 树组件。该组件可以接收树节点数据,并根据用户的选择返回选中的节点数据。我们使用了 React 和 MUI(Material-UI)库来实现这个功能,并按照前提、思路和步骤的顺序进行了解析和实现。

希望本篇博客对你理解如何使用 React 和 MUI 创建多选 Checkbox 树组件有所帮助!如果你有任何问题或建议,请随时留言。谢谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1365364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Transformer从菜鸟到新手(四)

引言 上篇文章完成了Transformer剩下组件的编写&#xff0c;因此本文就可以开始训练。 本文主要介绍训练时要做的一些事情&#xff0c;包括定义损失函数、学习率调整、优化器等。 下篇文章会探讨如何在多GPU上进行并行训练&#xff0c;加速训练过程。 数据集简介 从网上找到…

Java-IO-文件操作-FAQ-listlistFiles

1 需求 2 接口 3 示例 import java.io.File;public class Test {public static void main(String[] args) {System.out.println("***** ***** list() ***** *****");for (String s : new File("C:\\Windows\\System32\\drivers\\etc").list()) {System.ou…

【基础篇】十五、JVM垃圾回收器

文章目录 1、垃圾回收器2、Serial Serial Old3、ParNew回收器4、CMS回收器5、Parallel Scavenge Parallel Old6、G1垃圾回收器7、G1的回收流程&#xff1a;Young GC8、G1的回收流程&#xff1a;Mixed GC9、回收器的选择 1、垃圾回收器 对回收算法落地&#xff0c;出现了多种…

YOLOv5改进 | 损失函数篇 | InnerIoU、InnerSIoU、InnerWIoU、FocusIoU等损失函数

一、本文介绍 本文给大家带来的是YOLOv5最新改进,为大家带来最近新提出的InnerIoU的内容同时用Inner的思想结合SIoU、WIoU、GIoU、DIoU、EIOU、CIoU等损失函数,形成 InnerIoU、InnerSIoU、InnerWIoU等新版本损失函数,同时还结合了Focus和AIpha思想,形成的新的损失函数,其…

让充电器秒供多个快充口,乐得瑞推出1拖2功率分配快充线方案

随着PD3.1协议的市场应用越来越多&#xff0c;一些充电器的Type-C接口的输出功率达到百瓦及以上&#xff0c;如何充分利用好这类充电器设备&#xff0c;乐得瑞电子推出1拖2快充线缆解决方案&#xff0c;支持智能功率分配策略支持私有快充协议。 如上图是乐得瑞1拖2功率分配快充…

绝地求生:【违规处罚工作公示】12月25日-12月31日

12月25日至12月31日期间&#xff0c;共计对63,907个违规账号进行了封禁&#xff0c;其中53,880个账号因使用外挂被永久封禁。 若您游戏中遇到违规行为&#xff0c;建议您优先在游戏内进行举报&#xff1b; 另外您也可以在官方微信公众号【PUBG国际版】中点击“ 服务中心 - 举报…

实例:NodeJS 操作 Kafka

本人是C#出身的程序员&#xff0c;c#很简单就能实现&#xff0c;有需要的可以加我私聊。但是就目前流行的开发语言&#xff0c;尤其是面向web方向应用的&#xff0c;我感觉就是Nodejs最简单了。下面介绍&#xff1a; 本文将会介绍在windows环境下启动Kafka&#xff0c;并通过n…

《操作系统导论》笔记

操作系统三个关键&#xff1a;虚拟化( virtualization) 并发(concurrency) 持久性&#xff08;persistence&#xff09; 1 CPU虚拟化 1.1 进程 虚拟化CPU&#xff1a;许多任务共享物理CPU&#xff0c;让它们看起来像是同时运行。 时分共享&#xff1a;运行一个进程一段时间…

Python绘制茎叶图:plt.stem

文章目录 简介参数演示 简介 茎叶图从外观来看&#xff0c;更像是火柴&#xff0c;由基线、茎线、茎头三部分构成。最简单的示例如下 import numpy as np import matplotlib.pyplot as plt plt.stem(np.sin(np.arange(10))) plt.show()参数 stem的完整参数如下 stem([locs,…

MyBatis初学者必看:快速上手,让你的项目事半功倍!

为什么使用MyBatis 在Java程序中去连接数据库&#xff0c;最原始的办法是使用JDBC的API。我们先来回顾一下使用JDBC的方式&#xff0c;我们是如何操作数据库的。 Connection conn null; Statement stmt null; Blog blog new Blog();try {// 注册 JDBC 驱动Class.forName(&…

进阶学习——引导过程和服务控制

目录 一、引导过程 1.开机自检BIOS 2.MBR引导 3.GRUB菜单 4.加载Linux内核 5.init进程初始化 6.Centos启动过程总结 7.系统初始化进程 7.1init进程 7.2Systemd 7.2.1Systemd单元类型 7.2.2运行级别所对应的Systemd目标 二、服务控制 1.修复MBR扇区故障 1.1实验操…

前端超好玩的小游戏合集来啦--周末两天用html5做一个3D飞行兔子萝卜小游戏

文章目录 💖飞行兔子萝卜小游戏💟效果展示💟代码展示源码获取💖飞行兔子萝卜小游戏 💟效果展示 💟代码展示 <body> <script src=

欢乐钓鱼^^

欢迎来到程序小院 欢乐钓鱼 玩法&#xff1a;点击鼠标左键左右晃动的鱼钩&#xff0c;下方左右移动的鱼对准鱼的方向即可进行钓鱼&#xff0c; 不同的鱼不同的分数&#xff0c;快去钓鱼吧^^开始游戏https://www.ormcc.com/play/gameStart/241 html <div id"gamediv&qu…

力扣labuladong一刷day54天前缀树

力扣labuladong一刷day54天前缀树 文章目录 力扣labuladong一刷day54天前缀树一、208. 实现 Trie (前缀树)二、648. 单词替换三、211. 添加与搜索单词 - 数据结构设计四、1804. 实现 Trie &#xff08;前缀树&#xff09; II五、677. 键值映射 一、208. 实现 Trie (前缀树) 题…

[答疑]漏斗图,领域驱动设计叒创新了?

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 albert 2024-1-1 21:11 这篇文章说用DDD重构****&#xff0c;演示了一种漏斗图&#xff0c;请教潘老师&#xff0c;这个图是DDD提出来的吗&#xff0c;您怎么评价&#xff1f; UMLChi…

系列三十五、获取Excel中的总记录数

一、获取Excel中的总记录数 1.1、概述 使用EasyExcel开发进行文件上传时&#xff0c;通常会碰到一个问题&#xff0c;那就是Excel中的记录数太多&#xff0c;使用传统的方案进行文件上传&#xff0c;很容易就超时了&#xff0c;这时可以通过对用户上传的Excel中的数量进行限制…

优雅的通过Shell脚本生成Go的程序包

前言 随着Go语言的普及&#xff0c;越来越多的开发人员选择使用Go编写代码。虽然越来越多的公司项目已使用持续集成/持续部署&#xff08;CI/CD&#xff09;工具&#xff0c;用于自动化构建、测试和部署Go程序包&#xff0c;但存在一些部署在ECS服务器的Go程序包或需要手动编译…

编译原理Lab4-使用LightIR框架自动产生cminus-f语言的LLVM IR

[[#实验框架|实验框架]][[#实验过程|实验过程]] [[#实验过程#全局变量的设计|全局变量的设计]][[#实验过程#1ASTProgram|1ASTProgram]][[#实验过程#2ASTNum|2ASTNum]][[#实验过程#3ASTVarDeclaration|3ASTVarDeclaration]][[#实验过程#4ASTFunDeclaration|4ASTFunDeclaration]]…

概率论与数理统计 知识点+课后习题

文章目录 &#x1f496; [学习资源整合](https://www.cnblogs.com/duisheng/p/17872980.html)&#x1f4da; 总复习&#x1f4d5; 知识点⭐ 常用分布的数学期望和方差 &#x1f4d9; 选择题&#x1f4d9; 填空题&#x1f4d9; 大题1. 概率2. 概率3. 概率4. P5. 概率6. 概率密度…

读元宇宙改变一切笔记01_起源

1. 元宇宙是我们下一个生存之地 1.1. 1968年&#xff0c;只有不到10%的美国家庭拥有彩色电视&#xff0c;但当年票房排名第二位的电影《2001&#xff1a;太空漫游》&#xff08;2001: A Space Odyssey&#xff09;设想了这样的未来 1.1.1. 斯坦利库布里克(Stanley Kubrick) …