當前工作中,前端的主要技術棧用是vue。
那React怎么辦呢?總不至於把他扔在牆角吧!
只能在一些很小的項目上,也只有自己一個前端的時候,悄悄的上React。
當然,React項目UI組件還是最喜歡的Antd了。
近期的一個項目,就這么上了React和Antd,然后當中有一棵樹組件。
簡單看一下樹組件的設計圖吧!
看了設計圖,就發現一個小問題。
Antd組件庫當中的Tree組件子節點的向右縮進是通過父節點的padding-left實現的。那么就這么尷尬了,子節點的選中狀態背景色沒辦法占滿整行。如下圖:
這種情況最簡單的解決方案當然是跟設計師去協商,修改設計圖,讓設計圖的選中狀態符合Antd的Tree組件的選中狀態。
然而,就真的沒有別的辦法了?
再仔細看看Antd的各個組件。你會發現,有那么一個組件Menu(甚至不止一個組件,再看看Collapse),與當前的設計圖非常相似。是不是瞬間活力滿滿?
下面我們就用Menu來簡單重構一下當前的🌲組件。
看一下render函數:
render () {
const { openKeys, treeData } = this.state
return (
<Menu
mode="inline"
openKeys={this.state.openKeys}
onOpenChange={this.onOpenChange}
theme="dark"
inlineIndent={16}
style={{ width: 200 }}
className="nav-menu"
>
{
treeData && this.renderTree(treeData, openKeys)
}
</Menu>
)
}
真沒什么,也就是簡單的Menu組件的使用。
唯一需要注意的是,一般情況下,樹組件層級會稍微多一點,所以需要使用遞歸函數調用一下,而不應該是自己一個個<Menu.Item key={node.id}>{node.name}</Menu.Item>
寫下去。
然后多數情況下,這么久搞定了。但是,很顯然,這只是一個最簡單🌲組件,有一個非常常見的正常需求:就是只展開當前選擇所有父節點,而自動閉合其他節點。
這就是我們上述代碼當中openKeys={this.state.openKeys} onOpenChange={this.onOpenChange}
需要干的活。
按照官方文檔(官方文檔是提供了只展開一個父節點的示例的):
rootSubmenuKeys = ['sub1', 'sub2', 'sub4'];
state = {
openKeys: ['sub1'],
};
onOpenChange = openKeys => {
const latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1);
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
this.setState({ openKeys });
} else {
this.setState({
openKeys: latestOpenKey ? [latestOpenKey] : [],
});
}
};
然而問題也出在這里,這個示例,只能實現最頂層的節點只展開唯一父節點的需求,然而如果有多層節點,深層的節點,也需要如此功能,應該怎么辦呢?只能自己實現了。
關鍵點還是在openKeys
和onOpenChange
,openKeys
是當前展開的 SubMenu 菜單項 key 數組
,onOpenChange
是SubMenu 展開/關閉的回調
,那么就在onOpenChange
的時候做文章吧!
按照示例當中,獲取到當前的latestOpenKey
,就能夠確定當前的操作時閉合還是展開Submenu。
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1)
是表示閉合Submenu,這里不需要我們多做什么判斷,也就是可以稍微優化一下這個條件判斷(后面再說),我們主要的目標放在展開Submenu的邏輯當中。
想象一下,我們點擊每一個Submenu,是不是應該展開所有該節點的父節點?
那么我們怎么找到當前節點的父節點呢?
樹組件的數據,多數情況下都是以下這種類型的:
[
{
"name":"parent",
"id": "1111",
"children": [
{
"name": "parent1",
"id": "1112",
"childre": []
}
]
},
{
"name":"parent2",
"id": "1112"
}
]
甭管里面屬性名稱是啥,終歸他基本上是這么一層層往下嵌套的,那么依據當前節點,找到父節點,在這種數據結構下,各種循環遍歷就非常困難了。
最簡單的做法,當然是將當前數組結構全部展開,只要擁有children屬性的節點,我們都把他拖出來,組成一個新的數組。類似於如下格式:
[
{
"name":"parent",
"id": "1111",
"children": [
{
"name": "parent1",
"id": "1112",
"childre": []
}
]
},
{
"name":"parent2",
"id": "1112"
},
{
"name": "parent1",
"id": "1112",
"childre": []
}
]
這個過程就不詳細敘述了。
然后就是碼代碼的過程了。
我們通過onOpenChange,每次都是可以獲取到當前展開的openKeys的。我們在state當中也存儲了上一次展開的openKeys,那么就是比對這兩個openKeys,如果onOpenChange函數返回的openKeys全都在this.state.openKeys當中,那么就表示沒有展開新的Submenu。如果有一個不在this.state.openKeys當中,那這個不在this.state.openKeys當中的key就是我們當前展開的Submenu。這就是示例代碼const latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1)
所干的事情了,找到latestOpenKey。
當找到這個latestOpenKey之后,我們就去this.state.openKeys當中查找,這個latestOpenKey是this.state.openKeys哪個key節點的子節點。
而我們的this.state.openKeys存儲的數據其實是這樣的:[grandPa.id, parant.id, son.id,...]
,那么找latestOpenKey,就應該從后往前逐級向上查找父節點,一旦找到latestOpenKey的父節點的key,那么就截取,例如:找到當前latestOpenKey是grandPa節點的子節點,那么就存儲為[grandPa.id, latestOpenKey]
。
還是用代碼來實現吧。
// 重置openKeys
resetOpenKeys = key => {
let nodeKeys = []
let { openKeys, mapTreeData } = this.state
// 由於只展開一個父節點,所以openkeys存儲的類型必然是從父節點往子節點一級級存儲的
// 查找key為某一節點的子節點,應當從當前已知的最后一層節點一層層向上查找
// 所以需要reverse當前存儲順序openKeys然后再行遍歷
// openKeys = openKeys.reverse()
openKeys.reverse().forEach((item, index) => {
// mapTreeData是將樹形結構全部展開存儲在同一個數組當中,以便查找到當期那操作的節點
const target = mapTreeData.find(node => node.id === item)
if (target) {
// 查找當前展開的節點key是屬於那一層節點的子節點
const isExist = target.children.some(node => node.id === key)
if (isExist) {
// 一旦找到當前展開節點所屬的子節點就不再向上查找,並從當前openkeys的index截取
nodeKeys = openKeys.slice(index).reverse()
// return false
}
}
})
return [...nodeKeys, key]
}
mapTreeData就是上文說到的將tree結構展開的數據結構,以方便查找。
resetOpenKeys
返回的就是最終需要展開的Submenu的keys,這時候就可以修正onOpenChange函數了。
onOpenChange = openKeys => {
const latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1)
const subMenuData = this.state.mapTreeData
if (subMenuData.every(node => node.id !== latestOpenKey)) {
this.setState({ openKeys })
} else {
let openNewKeys = []
openNewKeys = this.resetOpenKeys(latestOpenKey)
this.setState({
openKeys: latestOpenKey.length ? [...openNewKeys] : [],
})
}
}
同時修正了前面提到的閉合Submenu時的條件判斷。subMenuData.every(node => node.id !== latestOpenKey)
。