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

在本篇博客中,我们将使用 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 树组件有所帮助!如果你有任何问题或建议,请随时留言。谢谢阅读!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

汇编反外挂

在软件保护领域&#xff0c;尤其是游戏保护中&#xff0c;反外挂是一个重要的议题。外挂通常指的是一种第三方软件&#xff0c;它可以修改游戏数据、操作游戏内存或提供其他作弊功能&#xff0c;从而给玩家带来不公平的优势。为了打击外挂&#xff0c;游戏开发者会采取一系列措…

计算机网络-网络互联

文章目录 网络互联网络互联方法LAN-LAN&#xff1a;网桥及其互连原理使用网桥实现LAN-LAN使用交换机扩展局域网使用路由器连接局域网 LAN-WANWAN-WAN路由选择算法非自适应路由选择算法自适应路由选择算法广播路由选择算法&#xff1a;分层路由选择算法 网络互联 网络互联是指利…

数据存储-文件存储

一、CSV文件存储 csv是python的标准库 列表数据写入csv文件 import csvheader [班级, 姓名, 性别, 手机号, QQ] # 二维数组 rows [[学习一班, 大娃, 男, a130111111122, 987456123],[学习二班, 二娃, 女, a130111111123, 987456155],[学习三班, 三娃, 男, a130111111124, …

Javase补充-Arrays类的常用方法汇总

文章目录 一 . 排序方法二 . 查找方法三 . 判断是否相等的方法四 . 拷贝方法五 . 填充方法 一 . 排序方法 我们第一个要介绍的就是sort方法 这个排序实现的底层逻辑应该是十分复杂的,以我们目前的水平体系应该无法理解,我们今天尝试用我们可以理解的一种排序算法,插入排序来模…

jQuery瀑布流画廊,瀑布流动态加载

jQuery瀑布流画廊&#xff0c;瀑布流动态加载 效果展示 手机布局 jQuery瀑布流动态加载 HTML代码片段 <!-- mediabanner --><div class"mediabanner"><img src"img/mediabanner.jpg" class"bg"/><div class"text&qu…

纽约纳斯达克大屏投放受众群体有哪些-大舍传媒

纽约纳斯达克大屏投放受众群体有哪些-大舍传媒 1. 纳斯达克大屏的概述 纳斯达克大屏是全球金融市场中最出名的电子交易平台之一。作为一个重要的金融信息传递渠道&#xff0c;纳斯达克大屏吸引了来自全球的投资者的目光。在这个巨大的投放平台上&#xff0c;大舍传媒希望为客…

Stable Diffusion 3的到来巩固了 AI 图像对抗 Sora 和 Gemini 的早期领先优势

Stability AI 将其更改为 Stable Diffusion 3。VentureBeat 报道称&#xff0c;Stability AI 的下一代旗舰 AI 图像生成模型将使用类似于 OpenAI 的 Sora 的扩散变压器框架。其当前模型仅依赖于扩散架构。虽然尚未发布&#xff0c;但您可以在等候名单中注册。 官方网址链接&am…

【JavaScript 漫游】【020】DOM 常用知识点总结

文章简介 DOM 是 JavaScript 操作网页的接口&#xff0c;全称为文档对象模型&#xff08;Document Object Model&#xff09;。DOM 操作是 JavaScript 最常见的任务&#xff0c;离开了 DOM&#xff0c;JavaScript 就无法操作网页。 本篇文章为【JavaScript 漫游】专栏的第 02…

设计模式--单例模式--懒汉饿汉

单例模式 单例模式(Singleton)&#xff0c;保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 单例模式 通常我们可以让一个全局变量使得一个对象被访问&#xff0c;但它不能防止你实例化多个对象。一个最好的办法就是&#xff0c;让类自身负责保存它的唯一实…

抖音数据抓取工具|短视频下载工具|视频内容提取软件

一、开发背景&#xff1a; 随着抖音平台的流行&#xff0c;越来越多的人希望能够下载抖音视频以进行个人收藏或分享。然而&#xff0c;目前在网上找到的抖音视频下载工具功能单一&#xff0c;操作繁琐&#xff0c;无法满足用户的需求。因此&#xff0c;我们决定开发一款功能强大…

完美解决ubuntu+windows双系统下时间不正确问题

在同一台电脑上安装ubuntuwindows双系统时&#xff0c;会出现某个系统的时间不正确的问题&#xff0c;而由于windows同步时间实在是太慢了&#xff0c;如果不去解决&#xff0c;windows上的时间大概率一直都是不对的。 原因分析 windows采用LocalTime机制设置时间&#xff0c…

挑战杯 基于大数据的股票量化分析与股价预测系统

文章目录 0 前言1 课题背景2 实现效果3 设计原理QTChartsarma模型预测K-means聚类算法算法实现关键问题说明 4 部分核心代码5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于大数据的股票量化分析与股价预测系统 该项目较为新颖…

onlyoffice api开发

编写代码 按照https://api.onlyoffice.com/editors/basic编写代码 <html> <head><meta charset"UTF-8"><meta name"viewport"content"widthdevice-width, user-scalableno, initial-scale1.0, maximum-scale1.0, minimum-scal…

【编译原理】用Python实现LR(0)语法分析

实验内容 对于给定的如下文法&#xff0c;编写调试一个上下文无关文法的LR(0)分析程序。 文法G’为&#xff1a; S → E S\to E S→E E → a A E\to aA E→aA E → b B E \to bB E→bB A → c A A\to cA A→cA A → d A\to d A→d B → c B B\to cB B→cB B → d B\to …

【C#】CNC 机器人的刀具路径生成软件PathCAM源码解析-Geometry

1. Loaders 1.1 DAE_Loader.cs 1.2 OBJ_Loader.cs 1.3 STL_Loader.cs 2. AnalyzedTriangleMesh.cs AnalyzedTriangleMesh类是一个用于分析和处理三角形网格&#xff0c;可以被用于将网格拆分为更小的部件或者识别特定特征的对象&#xff0c;如打印准备或几何分析&#xff0c;非…

网络原理——HTTP

1. 什么是HTTP协议 HTTP是应用层的协议。Java最主要的应用场景是做网站&#xff0c;而网站由 后端&#xff08;HTTP服务器&#xff09; 和 前端&#xff08;浏览器&#xff09;组成&#xff0c;HTTP协议就是负责这里后端和前端的数据交互。 HTTP3.0 之前在传输层是通过 TCP传…

如何移除禁用WordPress默认小工具(附WordPress默认小工具名称)

WordPress 自带的小工具非常多&#xff0c;但是我们用到的也就那么几种&#xff0c;甚至一种都不会用到&#xff0c;所以很有必要注销&#xff08;去除&#xff09;掉一些不用的小工具。实现的方法也很简单&#xff0c;只需将以下代码&#xff0c;根据自己的情况删除需要用的小…

黄仁勋最新专访:机器人基础模型可能即将出现,新一代GPU性能超乎想象

最近&#xff0c;《连线》的记者采访了英伟达CEO黄仁勋。 记者表示&#xff0c;与Jensen Huang交流应该带有警告标签&#xff0c;因为这位Nvidia首席执行官对人工智能的发展方向如此投入&#xff0c;以至于在经过近 90 分钟的热烈交谈后&#xff0c;我&#xff08;指代本采访的…

复旦大学MBA:AIGC时代,科技与商业迸发更绚烂的火花

ChatGPT问世以来&#xff0c;AI技术及应用进入一个全速推进的通道&#xff0c;快速迈入通用大模型时代。从AGI(人工通用智能&#xff09;到AIGC(AI多模态内容生成&#xff09;&#xff0c;AI正在飞速重塑各个行业、人类生活乃至人类的未来。在商业领域更是给营销场景和营销工具…

134 Linux 系统编程11 ,readlink命令,文件目录rwx权限差异,目录操作函数

一 readlink 命令 前面知道&#xff0c;如果a.soft是一个软链接&#xff0c;我们使用 cat a.soft,会直接查看这个软链接指向的文件 那么我们就是想看这个软链接是啥&#xff0c;可以使用 readlink a.soft 二 获取工作目录 getcwd函数 获取进程当前工作目录 (卷3&#xff0c;标…