使用React封裝一個(gè)Tree樹(shù)形組件的實(shí)例代碼
前言
為什么要造這樣一個(gè)輪子呢?
最近在學(xué)習(xí) next
,想用 next
重構(gòu)一下自己的博客,而在 自己博客 的編輯頁(yè)面中有使用到 antd
的一個(gè)樹(shù)形的結(jié)構(gòu)組件來(lái)展示文章的分類(lèi);
而我的 個(gè)人博客 (next版) 使用的是 next-ui
,但是里面并沒(méi)有 tree
組件,看了下最近很火的 shadcn
也沒(méi)有類(lèi)似組件,我也不想為了 tree
又引入 antd
了,就想著自己封裝一個(gè)玩玩,權(quán)當(dāng)提升技術(shù)了(當(dāng)然了非 next
版)。順便還能為 我的組件庫(kù) 添加一員。
當(dāng)然我是對(duì)照 antd
作為模板開(kāi)發(fā)的,但是他的 tree
是沒(méi)有單獨(dú) check
的,當(dāng)時(shí)我的舊版博客中為了實(shí)現(xiàn)該需求我可沒(méi)少費(fèi)工夫。
實(shí)現(xiàn)思路
我這里主要是根據(jù) antd
的 Props
選擇一部分,并按照自身需求來(lái)增減實(shí)現(xiàn)的。
下面我就講述整個(gè) tree
樹(shù)形組件的核心部分吧,其他一些屬性就不細(xì)講了,感興趣可以直接看 源碼 。
Html 基本結(jié)構(gòu)
下面是整個(gè)組件的基本結(jié)構(gòu),renderTreeList
函數(shù)遞歸調(diào)用渲染 tree
的 children
節(jié)點(diǎn)。
類(lèi)名 node-content
中的就是節(jié)點(diǎn)的內(nèi)容了,根據(jù)需求樣式自定義即可。
const Tree = forwardRef<TreeInstance, TreeProps>((props, ref) => { const { checkable, treeData, checkedKeys, defaultExpandAll, multiple, singleSelected, selectable = true, selectedKeys: propsSelectedKeys, onCheck, onSelect, onRightClick, ...ret } = props // ... 省略部分內(nèi)容,只展示核心結(jié)構(gòu) // 遞歸渲染 tree 的列表 const renderTreeList = (list?: TreeNode[]) => { // checkTree 的說(shuō)明見(jiàn)下面 if(!checkTree) return null return list?.map(item => { const checkItem = checkTree![item.key] return ( <div key={item.key} className={`node`}> <div className={`node-content`}> // checkItem.show 用來(lái)判斷展開(kāi) <div>↓</div> // checkItem.checked 用來(lái)處理是否 check <Checkbox /> <div>{item.title}</div> </div> <div className='children'> {renderTreeList(item.children)} </div> </div> ) }) } return ( <div className={`${classPrefix} ${ret.className ?? ''}`} style={ret.style}> {renderTreeList(treeData)} </div> ) })
實(shí)現(xiàn)交互的樹(shù)形結(jié)構(gòu) (checkTree)
生成一個(gè)用于實(shí)現(xiàn)交互效果的樹(shù)形結(jié)構(gòu) ( checkTree
)
export type CheckTreeItem = { /** 父節(jié)點(diǎn)的 key 值 */ parentKey?: string /** 子節(jié)點(diǎn)的 key 數(shù)組 */ childKeys?: string[] /** 是否展開(kāi) */ show: boolean /** 是否選中 */ checked: boolean /** 是否有 checkbox */ checkable?: boolean /** 禁用 checkbox */ disableCheckbox?: boolean /** 禁止整個(gè)節(jié)點(diǎn)的選擇 */ disabled?: boolean } export type CheckTree = Record<string, CheckTreeItem> // ... const [checkTree, setCheckTree] = useState<CheckTree>();
整體是一個(gè)只有一層結(jié)構(gòu)的對(duì)象,使用每一項(xiàng)數(shù)據(jù)中唯一的 key
值作為 checkTree
的 key
,通過(guò) parentKey
和 childKeys
來(lái)查找該節(jié)點(diǎn)的 父子兄弟節(jié)點(diǎn)
。
例:
初始化樹(shù)形結(jié)構(gòu)
根據(jù) generateCheckTree
函數(shù)的遞歸調(diào)用,將傳入的 treeData
樹(shù)狀結(jié)構(gòu)數(shù)據(jù)轉(zhuǎn)變?yōu)榻M件需要的 checkTree
。
// ... useEffect(() => { if(!treeData?.length) return const generateCheckTree = (list: TreeNode[], parentKey?: string) => { return list?.reduce((pre, cur) => { // checkedKeys 就是默認(rèn)傳入 check 項(xiàng),用于默認(rèn)是否勾選 const curChecked = Boolean(checkedKeys?.includes(cur.key)); pre[cur.key] = { // 默認(rèn)是否展開(kāi)該樹(shù)形結(jié)構(gòu) show: !!defaultExpandAll, checked: curChecked, parentKey, } // 一些屬性的默認(rèn)值 if(cur.checkable) pre[cur.key].checkable = true if(cur.disableCheckbox) pre[cur.key].disableCheckbox = true if(cur.disabled) pre[cur.key].disabled = true // 有孩子節(jié)點(diǎn)就遞歸調(diào)用,生成數(shù)據(jù) if(cur.children?.length) { pre[cur.key].childKeys = cur.children.map(c => c.key) const treeChild = generateCheckTree(cur.children, cur.key) pre = {...pre, ...treeChild} } return pre }, {} as CheckTree) } const state = generateCheckTree(treeData) setCheckTree(state) // ... }, [treeData])
大致就是如下圖所示,將 treeData
轉(zhuǎn)變?yōu)?nbsp;checkTree
。
點(diǎn)擊 check 節(jié)點(diǎn)
對(duì)應(yīng)上面 html 結(jié)構(gòu)中的 CheckBox 位置, checkable 等屬性就是用來(lái)判斷是否展示禁用 CheckBox 的。
// ... {(checkable && item.checkable !== false) && ( <CheckBox checked={checkItem.checked} disabled={item.disabled || item.disableCheckbox} // 先忽略,用來(lái)判斷當(dāng)前是否有孩子節(jié)點(diǎn)被選中了,true 則代表需要展示 checkbox 的半選樣式 indeterminate={getIsSomeChildCheck(checkItem, checkTree)} onChange={() => { if(item.disabled || item.disableCheckbox) return onNodeCheck(item.key) }} /> )}
先看 onChange
中觸發(fā)的回調(diào) onNodeCheck
函數(shù),該函數(shù)主要是將 checkItem
中對(duì)應(yīng)該項(xiàng)的 checked
取反一下。
/** 點(diǎn)擊選中節(jié)點(diǎn) */ const onNodeCheck = (key: string) => { const checkItem = checkTree![key] const curChecked = !checkItem.checked checkItem.checked = curChecked; // 先忽略,用來(lái)判斷是否是單選的 if(singleSelected) onSingleCheck(key, curChecked) else onCheckChildAndParent(key, curChecked) setCheckTree({...checkTree}) // 先忽略,用來(lái)獲取當(dāng)前 check 的所有 key 值 const keys = getCheckKeys(checkTree!) // check 觸發(fā)的組件回調(diào) onCheck?.(keys, { key, // 這步判斷主要是單選時(shí),選擇父節(jié)點(diǎn)時(shí)只會(huì)選中其子節(jié)點(diǎn) checked: keys.includes(key) ? curChecked : false, parentKeys: getParentKeys(key, checkTree!), treeDataItem: getTreeDataItem(key, treeData), }) }
然后通過(guò) onCheckChildAndParent
函數(shù),處理對(duì)應(yīng)的父子節(jié)點(diǎn)的選中狀態(tài)。
子節(jié)點(diǎn):
checkAllChild
遞歸將當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)
全選或全不選。父節(jié)點(diǎn):
checkAllParent
遞歸處理當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)
的選中狀態(tài)。兄弟節(jié)點(diǎn):只有在單選節(jié)點(diǎn)的時(shí)候需要,選擇同層節(jié)點(diǎn),使
兄弟節(jié)點(diǎn)
取消選中
/** 處理父子節(jié)點(diǎn)的選中狀態(tài) */ const onCheckChildAndParent = (key: string, curChecked: boolean, cTree = checkTree!) => { const checkItem = cTree[key]; // 全選/不選所有子節(jié)點(diǎn) (function checkAllChild(childKeys?: string[]) { childKeys?.forEach(childKey => { cTree[childKey].checked = curChecked checkAllChild(cTree[childKey].childKeys) }) })(checkItem.childKeys); // 處理父節(jié)點(diǎn)的選中狀態(tài) (function checkAllParent(parentKey?: string) { if(!parentKey) return if(!curChecked) { // 取消所有父節(jié)點(diǎn)的選中 cTree[parentKey].checked = false checkAllParent(cTree[parentKey].parentKey) } else { // 將所有子節(jié)點(diǎn)被全選的父節(jié)點(diǎn)也選中 const isSiblingCheck = !!cTree[parentKey].childKeys?.every(childKey => cTree[childKey].checked) if(isSiblingCheck) { // 判斷兄弟節(jié)點(diǎn)是否也全被選中 cTree[parentKey].checked = true checkAllParent(cTree[parentKey].parentKey) } } })(checkItem.parentKey); // 同層單選時(shí),使兄弟節(jié)點(diǎn)取消選中 if(singleSelected && curChecked) { const keys = cTree[key].parentKey ? cTree[cTree[key].parentKey!].childKeys : firstNodeKeys keys?.forEach(siblingKey => { if(siblingKey !== key) { cTree[siblingKey].checked = false } }) } }
子節(jié)點(diǎn)的展開(kāi)實(shí)現(xiàn)
html 結(jié)構(gòu)和 css 簡(jiǎn)單樣式如下,通過(guò) show
屬性給 children
節(jié)點(diǎn)賦高度,由于定義了 transition
屬性,所以當(dāng)高度變化時(shí),就會(huì)觸發(fā)節(jié)點(diǎn)的 展開(kāi)/收縮
動(dòng)畫(huà)。
<div className={`node-children`} // height: fit-content; 無(wú)法觸發(fā)過(guò)渡效果,需要準(zhǔn)確的值 // 也可通過(guò) maxHeight 設(shè)置一個(gè)很大的值來(lái)解決,但值過(guò)大又會(huì)使過(guò)度效果難看,所以這里需要獲取一個(gè)準(zhǔn)確的高度 style={{maxHeight: checkItem.show ? `${getTreeChildHeight(item.children!)}px` : 0}} > {renderTreeList(item.children)} </div>
.node-children { padding-left: 24px; overflow-y: hidden; transition: max-height 0.3s ease; }
這里有一個(gè)點(diǎn)要注意,就是無(wú)法直接給子節(jié)點(diǎn)定義一個(gè)由內(nèi)容撐開(kāi)的高度 height: fit-content;
,這樣會(huì)使 transition
無(wú)法正常觸發(fā)。當(dāng)然可以通過(guò)給一個(gè)比較大的 maxHeight
來(lái)設(shè)置最大高度,這樣 transition
就會(huì)以 maxHeight
的高度實(shí)現(xiàn)動(dòng)畫(huà)效果,但是這樣當(dāng)子節(jié)點(diǎn)總高度和 maxHeight
出入過(guò)大時(shí)就會(huì)使動(dòng)畫(huà)效果很不好看。
所以我這里最終通過(guò) getTreeChildHeight
函數(shù)來(lái)準(zhǔn)確計(jì)算孩子節(jié)點(diǎn)的總高度了。
首先等待 checkTree
完成構(gòu)建以及樹(shù)形結(jié)構(gòu)渲染完成,然后準(zhǔn)確獲取每個(gè)節(jié)點(diǎn)的高度,因?yàn)槊總€(gè)節(jié)點(diǎn)的 title
都是 ReactNode
,所以需要都獲取一遍他們的高度。
/** 標(biāo)題的最小高度 */ const TITLE_MIN_HEIGHT = 24; /** 每個(gè)標(biāo)題的下邊距 */ const TITLE_MB = 6; // 等待樹(shù)形結(jié)構(gòu)渲染完畢,獲取 title 的高度 useEffect(() => { if(!checkTree || !isTreeRender.current) return const info: TitleNodeInfo = {}; for(let key in checkTree) { // 每個(gè)標(biāo)題渲染的內(nèi)容,都要根據(jù) key 給一個(gè)唯一的類(lèi)名。 const titleNode = document.querySelector(`.node-title-${key}`) if(titleNode) { info[key] = {height: Math.max(titleNode.clientHeight, TITLE_MIN_HEIGHT) + TITLE_MB} } } setTitleNodeInfo(info) isTreeRender.current = false }, [checkTree])
此時(shí)每個(gè)節(jié)點(diǎn)的 children 節(jié)點(diǎn)的高度,就能通過(guò) getTreeChildHeight
函數(shù)遞歸計(jì)算得出了。
const getTreeChildHeight = (list: TreeNode[]) => { return list?.reduce((pre, cur) => { pre += (titleNodeInfo[cur.key]?.height ?? (TITLE_MIN_HEIGHT + TITLE_MB)) if(checkTree![cur.key].show && cur.children?.length) { pre += getTreeChildHeight(cur.children) } return pre }, 0) ?? 0 }
ref 方法
然后我在組件里面實(shí)現(xiàn)了一些用于獲取 treeData
數(shù)據(jù)的一些方法,簡(jiǎn)單來(lái)說(shuō)都是一些遞歸調(diào)用等方法。
屬性名 | 描述 | 類(lèi)型 |
---|---|---|
getCheckTree | 獲取當(dāng)前選中的樹(shù)形結(jié)構(gòu) | () => CheckTree | undefined |
getParentKeys | 根據(jù) key 值獲取其父節(jié)點(diǎn),從 key 節(jié)點(diǎn)的最親關(guān)系開(kāi)始排列 | (key: string) => string[] | undefined |
getSiblingKeys | 根據(jù) key 值獲取其兄弟節(jié)點(diǎn),會(huì)包括自身節(jié)點(diǎn) | (key: string) => string[] | undefined |
getChildKeys | 根據(jù) key 值獲取其子節(jié)點(diǎn) | (key: string) => string[] | undefined |
getCheckKeys | 獲取當(dāng)前 check 中的所有 key | () => string[] |
getTreeDataItem | 獲取當(dāng)前 treeData 中的節(jié)點(diǎn)數(shù)據(jù) | (key: string) => TreeNode | undefined |
最終實(shí)現(xiàn)的 Props
其他屬性的功能實(shí)現(xiàn)我就不一一敘述了,感興趣可以直接看 源碼
屬性名 | 描述 | 類(lèi)型 | 默認(rèn)值 |
---|---|---|---|
checkable | 是否有選擇框 | boolean | false |
checkedKeys | (受控)選中復(fù)選框的樹(shù)節(jié)點(diǎn)的key,當(dāng)不在數(shù)組中的父節(jié)點(diǎn)需要被選中時(shí),對(duì)應(yīng)節(jié)點(diǎn)也將選中,觸發(fā) onCheck 回調(diào),使該值保持正確 | string[] | null |
defaultExpandAll | 默認(rèn)展開(kāi)所有樹(shù)節(jié)點(diǎn) | boolean | false |
multiple | 支持點(diǎn)選多個(gè)節(jié)點(diǎn)(節(jié)點(diǎn)本身) | boolean | false |
singleSelected | 是否只能單選一個(gè)節(jié)點(diǎn) | boolean | false |
selectable | 是否可選中 | boolean | true |
selectedKeys | (受控)設(shè)置選中的樹(shù)節(jié)點(diǎn),多選需設(shè)置 multiple 為 true | string[] | "-" |
treeData | 樹(shù)形結(jié)構(gòu)的數(shù)據(jù) | TreeNode[] | -- |
onCheck | 點(diǎn)擊復(fù)選框觸發(fā) | (checkedKeys: string[], params?: OnCheckParams) => void | -- |
onSelect | 點(diǎn)擊樹(shù)節(jié)點(diǎn)觸發(fā) | (selectKeys: string[], params: OnSelectParams) => void | -- |
onRightClick | 點(diǎn)擊右鍵觸發(fā) | (params: onRightClickParams) => void | -- |
className | 類(lèi)名 | string | -- |
style | style樣式 | {} | -- |
children | children節(jié)點(diǎn) | ReactNode | -- |
ref | - | TreeInstance | -- |
以上就是使用React封裝一個(gè)Tree樹(shù)形組件的實(shí)例代碼的詳細(xì)內(nèi)容,更多關(guān)于React封裝Tree組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react native帶索引的城市列表組件的實(shí)例代碼
本篇文章主要介紹了react-native城市列表組件的實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08react 項(xiàng)目 中使用 Dllplugin 打包優(yōu)化技巧
在用 Webpack 打包的時(shí)候,對(duì)于一些不經(jīng)常更新的第三方庫(kù),比如 react,lodash,vue 我們希望能和自己的代碼分離開(kāi),這篇文章主要介紹了react 項(xiàng)目 中 使用 Dllplugin 打包優(yōu)化,需要的朋友可以參考下2023-01-01react組件實(shí)例屬性props實(shí)例詳解
這篇文章主要介紹了react組件實(shí)例屬性props,本文結(jié)合實(shí)例代碼給大家簡(jiǎn)單介紹了props使用方法,代碼簡(jiǎn)單易懂,需要的朋友可以參考下2023-01-01詳解使用React.memo()來(lái)優(yōu)化函數(shù)組件的性能
本文講述了開(kāi)發(fā)React應(yīng)用時(shí)如何使用shouldComponentUpdate生命周期函數(shù)以及PureComponent去避免類(lèi)組件進(jìn)行無(wú)用的重渲染,以及如何使用最新的React.memo API去優(yōu)化函數(shù)組件的性能2019-03-03react遞歸組件實(shí)現(xiàn)樹(shù)的示例詳解
在一些react項(xiàng)目中,常常有一些需要目錄樹(shù)這種結(jié)構(gòu),這篇文章主要為大家介紹了如何使用遞歸組件實(shí)現(xiàn)樹(shù),感興趣的小伙伴可以了解下2024-10-10JavaScript React如何修改默認(rèn)端口號(hào)方法詳解
這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號(hào)方法詳解,文中通過(guò)步驟圖片解析介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07