近期的一个项目,就这么上了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] : [],
    });
  }
};

然而问题也出在这里,这个示例,只能实现最顶层的节点只展开唯一父节点的需求,然而如果有多层节点,深层的节点,也需要如此功能,应该怎么办呢?只能自己实现了。

关键点还是在openKeysonOpenChangeopenKeys当前展开的 SubMenu 菜单项 key 数组onOpenChangeSubMenu 展开/关闭的回调,那么就在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)

posted on 2019-11-20 11:05  烛火星光  阅读(1310)  评论(0编辑  收藏  举报