Create a multi-selection Checkbox tree component using React and MUI

In this blog, we will use React and MUI (Material-UI) library to create a multi-selection Checkbox tree component. This component can be used to display tree-structured data and allow users to select multiple nodes.

premise

Before starting, make sure you have the following dependencies installed:

  • React
  • MUI(Material-UI)

final style

Not all selected

Insert image description here

Select all state

Insert image description here

Ideas

Our goal is to create a multi-selection Checkbox tree component that can receive tree node data and return selected node data based on the user's selection. To achieve this goal, we will follow the following steps:

  1. Create a React function component CheckBoxTreethat receives a dataproperty as the tree node data, and optionally receives a handleCheckDataproperty as a callback function for passing the selected node data.
  2. In the component's state, create an selectedarray to store the id of the selected node.
  3. Implement a onCheckfunction to handle the click event of the node Checkbox. In this function, we will update selectedthe array according to the user's selection and recursively handle the selected state of the child node.
  4. Implement a renderTreefunction for recursively rendering tree nodes. In this function, we will render the Checkbox and node name based on the selected state of the node and the number of child nodes.
  5. Use TreeViewand TreeItemcomponents to display the tree structure, and pass the tree node data to renderTreethe function for rendering.

step

The following are the detailed steps to implement the multi-selection Checkbox tree component:

1. Create React function component

First, we need to create a React function component CheckBoxTreeand define its properties and state. code show as below:

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. Split the parent node

Next, we define splitNodeIdthe function to split the node id into all parent node ids. It accepts a node id string, in the format '1_2_3', and returns an array of parent node ids, for example ['1_2', '1']. 3 represents the current node.

/**
 * 拆分节点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. Implement the click event processing function of the node Checkbox

Next, we need to implement a onCheckfunction to handle the click event of the node Checkbox. In this function, we will update selectedthe array according to the user's selection and recursively handle the selected state of the child node. code show as below:

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. Implement functions for recursively rendering tree nodes

Then, we need to implement a renderTreefunction that recursively renders the tree nodes. In this function, we will render the Checkbox and node name based on the selected state of the node and the number of child nodes. code show as below:

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. Render tree structure

Finally, we use TreeViewthe and TreeItemcomponents to display the tree structure and pass the tree node data to renderTreethe function for rendering. code show as below:

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

6. Complete code

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>
  );
}

Summarize

Through the above steps, we successfully created a multi-select Checkbox tree component. This component can receive tree node data and return selected node data based on the user's selection. We used React and MUI (Material-UI) libraries to implement this function, and analyzed and implemented it in the order of premise, ideas and steps.

I hope this blog will help you understand how to use React and MUI to create a multi-select Checkbox tree component! If you have any questions or suggestions, please feel free to leave a message. thanks for reading!

Guess you like

Origin blog.csdn.net/m0_73117087/article/details/135409717