詳解如何使用React和MUI創(chuàng)建多選Checkbox樹(shù)組件
在本篇博客中,我們將使用 React 和 MUI(Material-UI)庫(kù)來(lái)創(chuàng)建一個(gè)多選 Checkbox 樹(shù)組件。該組件可以用于展示樹(shù)形結(jié)構(gòu)的數(shù)據(jù),并允許用戶(hù)選擇多個(gè)節(jié)點(diǎn)。
前提
在開(kāi)始之前,確保你已經(jīng)安裝了以下依賴(lài):
- React
- MUI(Material-UI)
最終樣式
非全選狀態(tài)
全選狀態(tài)
思路
我們的目標(biāo)是創(chuàng)建一個(gè)多選 Checkbox 樹(shù)組件,它可以接收樹(shù)節(jié)點(diǎn)數(shù)據(jù),并根據(jù)用戶(hù)的選擇返回選中的節(jié)點(diǎn)數(shù)據(jù)。為了實(shí)現(xiàn)這個(gè)目標(biāo),我們將按照以下步驟進(jìn)行:
創(chuàng)建一個(gè) React 函數(shù)組件 CheckBoxTree,它接收一個(gè) data 屬性作為樹(shù)節(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ù)用戶(hù)的選擇更新 selected 數(shù)組,并遞歸處理子節(jié)點(diǎn)的選中狀態(tài)。
實(shí)現(xiàn)一個(gè) renderTree 函數(shù),用于遞歸渲染樹(shù)節(jié)點(diǎn)。在該函數(shù)中,我們將根據(jù)節(jié)點(diǎn)的選中狀態(tài)和子節(jié)點(diǎn)的數(shù)量來(lái)渲染 Checkbox 和節(jié)點(diǎn)名稱(chēng)。
使用 TreeView 和 TreeItem 組件來(lái)展示樹(shù)形結(jié)構(gòu),并將樹(shù)節(jié)點(diǎn)數(shù)據(jù)傳遞給 renderTree 函數(shù)進(jìn)行渲染。
步驟
下面是實(shí)現(xiàn)多選 Checkbox 樹(shù)組件的詳細(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)
接下來(lái),我們定義了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ù)
接下來(lái),我們需要實(shí)現(xiàn)一個(gè) onCheck 函數(shù),用于處理節(jié)點(diǎn) Checkbox 的點(diǎn)擊事件。在該函數(shù)中,我們將根據(jù)用戶(hù)的選擇更新 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)遞歸渲染樹(shù)節(jié)點(diǎn)的函數(shù)
然后,我們需要實(shí)現(xiàn)一個(gè) renderTree 函數(shù),用于遞歸渲染樹(shù)節(jié)點(diǎn)。在該函數(shù)中,我們將根據(jù)節(jié)點(diǎn)的選中狀態(tài)和子節(jié)點(diǎn)的數(shù)量來(lái)渲染 Checkbox 和節(jié)點(diǎn)名稱(chēng)。代碼如下:
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. 渲染樹(shù)形結(jié)構(gòu)
最后,我們使用 TreeView 和 TreeItem 組件來(lái)展示樹(shù)形結(jié)構(gòu),并將樹(shù)節(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 { // 樹(shù)節(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樹(shù)組件 * @param props 組件屬性 * @returns JSX組件 */ export default function CheckBoxTree(props: CheckBoxTreeProps) { // 獲取樹(shù)節(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 事件對(duì)象 * @param node 節(jié)點(diǎn)對(duì)象 * @param parentNodeName 父節(jié)點(diǎn)名稱(chēng) */ 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對(duì)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ū)域已無(wú)選中節(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); } }; /** * 遞歸渲染樹(shù)節(jié)點(diǎn) * @param nodes 樹(shù)節(jié)點(diǎn)數(shù)組 * @param parentNodeName 父節(jié)點(diǎn)名稱(chēng) * @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)槭峭ㄟ^(guò)parent id來(lái)綁定子元素,所以下面的元素是只返回最后的子元素 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é)
通過(guò)以上步驟,我們成功地創(chuàng)建了一個(gè)多選 Checkbox 樹(shù)組件。該組件可以接收樹(shù)節(jié)點(diǎn)數(shù)據(jù),并根據(jù)用戶(hù)的選擇返回選中的節(jié)點(diǎn)數(shù)據(jù)。我們使用了 React 和 MUI(Material-UI)庫(kù)來(lái)實(shí)現(xiàn)這個(gè)功能,并按照前提、思路和步驟的順序進(jìn)行了解析和實(shí)現(xiàn)。
以上就是詳解如何使用React和MUI創(chuàng)建多選Checkbox樹(shù)組件的詳細(xì)內(nèi)容,更多關(guān)于React MUI創(chuàng)建多選Checkbox樹(shù)組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React-View-UI組件庫(kù)封裝Loading加載中源碼
這篇文章主要介紹了React-View-UI組件庫(kù)封裝Loading加載樣式,主要包括組件介紹,組件源碼及組件測(cè)試源碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06React中完整實(shí)例講解Recoil狀態(tài)管理庫(kù)的使用
這篇文章主要介紹了React中Recoil狀態(tài)管理庫(kù)的使用,Recoil的產(chǎn)生源于Facebook內(nèi)部一個(gè)可視化數(shù)據(jù)分析相關(guān)的應(yīng)用,在使用React的實(shí)現(xiàn)的過(guò)程中,因?yàn)楝F(xiàn)有狀態(tài)管理工具不能很好的滿(mǎn)足應(yīng)用的需求,因此催生出了Recoil,對(duì)Recoil感興趣可以參考下文2023-05-05react中使用redux-persist做持久化儲(chǔ)存的過(guò)程記錄
這篇文章主要介紹了react中使用redux-persist做持久化儲(chǔ)存的相關(guān)資料,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01React Native 資源包拆分實(shí)戰(zhàn)分析
這篇文章主要為大家介紹了React Native 資源包拆分實(shí)戰(zhàn)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08useReducer使用詳解及其應(yīng)用場(chǎng)景
這篇文章主要介紹了useReducer使用詳解及其應(yīng)用場(chǎng)景,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03