Antd組件庫,利用Menu組件模擬一個簡單Tree組件



當前工作中,前端的主要技術棧用是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] : [],
    });
  }
};

然而問題也出在這里,這個示例,只能實現最頂層的節點只展開唯一父節點的需求,然而如果有多層節點,深層的節點,也需要如此功能,應該怎么辦呢?只能自己實現了。

關鍵點還是在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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM