詳解如何使用React和MUI創(chuàng)建多選Checkbox樹組件
在本篇博客中,我們將使用 React 和 MUI(Material-UI)庫來創(chuàng)建一個(gè)多選 Checkbox 樹組件。該組件可以用于展示樹形結(jié)構(gòu)的數(shù)據(jù),并允許用戶選擇多個(gè)節(jié)點(diǎn)。
前提
在開始之前,確保你已經(jīng)安裝了以下依賴:
- React
- MUI(Material-UI)
最終樣式
非全選狀態(tài)

全選狀態(tài)

思路
我們的目標(biāo)是創(chuàng)建一個(gè)多選 Checkbox 樹組件,它可以接收樹節(jié)點(diǎn)數(shù)據(jù),并根據(jù)用戶的選擇返回選中的節(jié)點(diǎn)數(shù)據(jù)。為了實(shí)現(xiàn)這個(gè)目標(biāo),我們將按照以下步驟進(jìn)行:
創(chuàng)建一個(gè) React 函數(shù)組件 CheckBoxTree,它接收一個(gè) data 屬性作為樹節(jié)點(diǎn)數(shù)據(jù),并可選地接收一個(gè) handleCheckData 屬性作為回調(diào)函數(shù),用于傳遞選中的節(jié)點(diǎn)數(shù)據(jù)。
在組件的狀態(tài)中,創(chuàng)建一個(gè) selected 數(shù)組,用于存儲(chǔ)選中的節(jié)點(diǎn)的 id。
實(shí)現(xiàn)一個(gè) onCheck 函數(shù),用于處理節(jié)點(diǎn) Checkbox 的點(diǎn)擊事件。在該函數(shù)中,我們將根據(jù)用戶的選擇更新 selected 數(shù)組,并遞歸處理子節(jié)點(diǎn)的選中狀態(tài)。
實(shí)現(xiàn)一個(gè) renderTree 函數(shù),用于遞歸渲染樹節(jié)點(diǎn)。在該函數(shù)中,我們將根據(jù)節(jié)點(diǎn)的選中狀態(tài)和子節(jié)點(diǎn)的數(shù)量來渲染 Checkbox 和節(jié)點(diǎn)名稱。
使用 TreeView 和 TreeItem 組件來展示樹形結(jié)構(gòu),并將樹節(jié)點(diǎn)數(shù)據(jù)傳遞給 renderTree 函數(shù)進(jìn)行渲染。
步驟
下面是實(shí)現(xiàn)多選 Checkbox 樹組件的詳細(xì)步驟:
1. 創(chuàng)建 React 函數(shù)組件
首先,我們需要?jiǎng)?chuàng)建一個(gè) React 函數(shù)組件 CheckBoxTree,并定義它的屬性和狀態(tài)。代碼如下:
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. 分割父節(jié)點(diǎn)
接下來,我們定義了splitNodeId函數(shù),用于將節(jié)點(diǎn)id拆分為所有父節(jié)點(diǎn)id。它接受一個(gè)節(jié)點(diǎn)id字符串,格式為'1_2_3',并返回一個(gè)父節(jié)點(diǎn)id數(shù)組,例如['1_2', '1']。3表示的是當(dāng)前節(jié)點(diǎn)。
/**
* 拆分節(jié)點(diǎn)id為所有父節(jié)點(diǎn)id
* @param id 節(jié)點(diǎn)id,格式為'1_2_3'
* @returns 父節(jié)點(diǎn)id數(shù)組,如['1_2', '1']
*/
function splitNodeId(id: string) {
// 按'_'分割節(jié)點(diǎn)id
const path = id.split('_');
// 累加生成父節(jié)點(diǎn)id
return path.reduce((result: string[], current) => {
// 拼接'_'和當(dāng)前節(jié)點(diǎn)
result.push(`${result.at(-1) ? result.at(-1) + '_' : ''}${current}`);
return result;
}, []);
}
3. 實(shí)現(xiàn)節(jié)點(diǎn) Checkbox 的點(diǎn)擊事件處理函數(shù)
接下來,我們需要實(shí)現(xiàn)一個(gè) onCheck 函數(shù),用于處理節(jié)點(diǎn) Checkbox 的點(diǎn)擊事件。在該函數(shù)中,我們將根據(jù)用戶的選擇更新 selected 數(shù)組,并遞歸處理子節(jié)點(diǎn)的選中狀態(tài)。代碼如下:
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. 實(shí)現(xiàn)遞歸渲染樹節(jié)點(diǎn)的函數(shù)
然后,我們需要實(shí)現(xiàn)一個(gè) renderTree 函數(shù),用于遞歸渲染樹節(jié)點(diǎn)。在該函數(shù)中,我們將根據(jù)節(jié)點(diǎn)的選中狀態(tài)和子節(jié)點(diǎn)的數(shù)量來渲染 Checkbox 和節(jié)點(diǎn)名稱。代碼如下:
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. 渲染樹形結(jié)構(gòu)
最后,我們使用 TreeView 和 TreeItem 組件來展示樹形結(jié)構(gòu),并將樹節(jié)點(diǎn)數(shù)據(jù)傳遞給 renderTree 函數(shù)進(jìn)行渲染。代碼如下:
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;
}
// 組件狀態(tài)
int
erface CheckboxTreeState {
// 選中節(jié)點(diǎn)id數(shù)組
selected: string[];
}
// 組件屬性
interface CheckBoxTreeProps {
// 樹節(jié)點(diǎn)數(shù)據(jù)
data: RegionType[];
// 向外傳遞選擇框數(shù)據(jù),
handleCheckData?: (data: string[]) => void;
}
/**
* 拆分節(jié)點(diǎn)id為所有父節(jié)點(diǎn)id
* @param id 節(jié)點(diǎn)id,格式為'1_2_3'
* @returns 父節(jié)點(diǎn)id數(shù)組,如['1_2', '1']
*/
function splitNodeId(id: string) {
// 按'_'分割節(jié)點(diǎn)id
const path = id.split('_');
// 累加生成父節(jié)點(diǎn)id
return path.reduce((result: string[], current) => {
// 拼接'_'和當(dāng)前節(jié)點(diǎn)
result.push(`${result.at(-1) ? result.at(-1) + '_' : ''}${current}`);
return result;
}, []);
}
/**
* 多選Checkbox樹組件
* @param props 組件屬性
* @returns JSX組件
*/
export default function CheckBoxTree(props: CheckBoxTreeProps) {
// 獲取樹節(jié)點(diǎn)數(shù)據(jù)
const { data, handleCheckData } = props;
// 組件狀態(tài):選中節(jié)點(diǎn)id數(shù)組
const [state, setState] = React.useState<CheckboxTreeState>({
selected: []
});
/**
* 點(diǎn)擊節(jié)點(diǎn)Checkbox觸發(fā)
* @param event 事件對象
* @param node 節(jié)點(diǎn)對象
* @param parentNodeName 父節(jié)點(diǎn)名稱
*/
const onCheck = (
event: React.ChangeEvent<HTMLInputElement>,
node: RegionType,
parentNodeName?: string
) => {
// 獲取Checkbox選中狀態(tài)
const { checked } = event.target;
// 當(dāng)前節(jié)點(diǎn)id
const currentId = parentNodeName ?
`${parentNodeName}_${node.id.id}` :
node.id.id;
// 父節(jié)點(diǎn)id數(shù)組
const parentAreaName = splitNodeId(currentId);
// 選中狀態(tài):選中當(dāng)前節(jié)點(diǎn)和父節(jié)點(diǎn)
if (checked) {
setState((prevState) => ({
...prevState,
//使用Set對selected數(shù)組去重
selected: Array.from(
new Set([...prevState.selected, ...parentAreaName])
)
}));
// 若有子節(jié)點(diǎn),遞歸選中
if (node.children && node.children.length > 0) {
node.children.forEach((item) => {
onCheck(event, item, currentId);
});
}
} else if (!checked) {
// 臨時(shí)state
let tempState = { ...state };
// 逆序遍歷,進(jìn)行選中狀態(tài)更新
for (let index = parentAreaName.length - 1; index >= 0; index--) {
const element = parentAreaName[index];
// 若父區(qū)域已無選中節(jié)點(diǎn),取消選中父區(qū)域
if (
tempState.selected.filter((id) => id.startsWith(`${element}_`))
.length === 0
) {
tempState = {
...tempState,
selected: tempState.selected.filter((id) => id !== element)
};
}
// 取消選中當(dāng)前區(qū)域
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);
}
};
/**
* 遞歸渲染樹節(jié)點(diǎn)
* @param nodes 樹節(jié)點(diǎn)數(shù)組
* @param parentNodeName 父節(jié)點(diǎn)名稱
* @returns JSX組件
*/
const renderTree = (nodes: RegionType, parentNodeName?: string) => {
// 子節(jié)點(diǎn)總數(shù)
let currentLength = 0;
/**
* 獲取子節(jié)點(diǎn)總數(shù)
* @param currentNodes 當(dāng)前節(jié)點(diǎn)
*/
function getNodeLength(currentNodes: RegionType) {
currentNodes.children?.forEach((node) => {
currentLength++;
if (node.children) {
getNodeLength(node);
}
});
}
// 當(dāng)前節(jié)點(diǎn)id
const currentId = parentNodeName ?
`${parentNodeName}_${nodes.id.id}` :
nodes.id.id;
// 獲取當(dāng)前節(jié)點(diǎn)子節(jié)點(diǎn)總數(shù)
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={
// 若有子節(jié)點(diǎn),判斷子節(jié)點(diǎn)是否全部選中
// 或節(jié)點(diǎn)自身是否選中
nodes.children &&
nodes.children.length &&
state.selected.filter((id) =>
id.startsWith(`${currentId}_`)
).length === currentLength ||
state.selected.some((id) => id === currentId)
}
indeterminate={
// 子節(jié)點(diǎn)存在選中與非選中狀態(tài)
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>
);
};
/**
* 組件加載時(shí)觸發(fā),獲取去重后的多選框id列表
*/
React.useEffect(() => {
// state.selected拆分?jǐn)?shù)組并合并,返回成一個(gè)數(shù)組,如果需要去重后的值,可以使用Array.from(new set)
const checkBoxList = state.selected.flatMap((item) => item.split('_'));
// 因?yàn)槭峭ㄟ^parent id來綁定子元素,所以下面的元素是只返回最后的子元素
const checkTransferList = checkBoxList.filter(
(value) => checkBoxList.indexOf(value) === checkBoxList.lastIndexOf(value)
);
// 從多選值數(shù)組中生成集合Set,再使用Array.from轉(zhuǎn)換為數(shù)組
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>
);
}總結(jié)
通過以上步驟,我們成功地創(chuàng)建了一個(gè)多選 Checkbox 樹組件。該組件可以接收樹節(jié)點(diǎn)數(shù)據(jù),并根據(jù)用戶的選擇返回選中的節(jié)點(diǎn)數(shù)據(jù)。我們使用了 React 和 MUI(Material-UI)庫來實(shí)現(xiàn)這個(gè)功能,并按照前提、思路和步驟的順序進(jìn)行了解析和實(shí)現(xiàn)。
以上就是詳解如何使用React和MUI創(chuàng)建多選Checkbox樹組件的詳細(xì)內(nèi)容,更多關(guān)于React MUI創(chuàng)建多選Checkbox樹組件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React Native 集成jpush-react-native的示例代碼
這篇文章主要介紹了React Native 集成jpush-react-native的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Reactjs?+?Nodejs?+?Mongodb?實(shí)現(xiàn)文件上傳功能實(shí)例詳解
今天是使用?Reactjs?+?Nodejs?+?Mongodb?實(shí)現(xiàn)文件上傳功能,前端我們使用?Reactjs?+?Axios?來搭建前端上傳文件應(yīng)用,后端我們使用?Node.js?+?Express?+?Multer?+?Mongodb?來搭建后端上傳文件處理應(yīng)用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06
React狀態(tài)管理的項(xiàng)目實(shí)踐
本文主要介紹了狀態(tài)管理的概念,包括本地狀態(tài)和全局狀態(tài)的管理方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2026-01-01
ReactDOM.render在react源碼中執(zhí)行原理
這篇文章主要為大家介紹了ReactDOM.render在react源碼中執(zhí)行原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
淺析history 和 react-router 的實(shí)現(xiàn)原理
react-router 版本更新非???但是它的底層實(shí)現(xiàn)原理確是萬變不離其中,在本文中會(huì)從前端路由出發(fā)到 react-router 原理總結(jié)與分享,本文對history 和 react-router實(shí)現(xiàn)原理講解的非常詳細(xì),需要的朋友跟隨小編一起看看吧2023-08-08
redux功能強(qiáng)大的Middleware中間件使用學(xué)習(xí)
這篇文章主要為大家介紹了redux功能強(qiáng)大的Middleware中間件使用學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
React Router中Link和NavLink的學(xué)習(xí)心得總結(jié)
這篇文章主要介紹了React Router中Link和NavLink的學(xué)習(xí)心得總結(jié),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
如何使用Next.js?+?Prisma?+?MySQL開發(fā)全棧項(xiàng)目
在Next.js項(xiàng)目的任何地方都可以使用Prisma來訪問數(shù)據(jù)庫,Prisma提供了豐富的查詢方法,滿足您在項(xiàng)目中的各種需求,這篇文章主要介紹了如何使用Next.js+Prisma+MySQL開發(fā)全棧項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2026-03-03

