近期的一个项目,就这么上了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)
。