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