欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用React封裝一個Tree樹形組件的實例代碼

 更新時間:2024年03月11日 10:58:06   作者:滑動變滾動的蝸牛  
這篇文章主要介紹了使用React封裝一個Tree樹形組件的實例,文中通過代碼示例講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下

前言

為什么要造這樣一個輪子呢?

最近在學(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)工夫。

線上 Demo

源碼

實現(xiàn)思路

我這里主要是根據(jù) antdProps 選擇一部分,并按照自身需求來增減實現(xiàn)的。

下面我就講述整個 tree 樹形組件的核心部分吧,其他一些屬性就不細(xì)講了,感興趣可以直接看 源碼 。

Html 基本結(jié)構(gòu)

下面是整個組件的基本結(jié)構(gòu),renderTreeList 函數(shù)遞歸調(diào)用渲染 treechildren 節(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是否有選擇框booleanfalse
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)booleanfalse
multiple支持點(diǎn)選多個節(jié)點(diǎn)(節(jié)點(diǎn)本身)booleanfalse
singleSelected是否只能單選一個節(jié)點(diǎn)booleanfalse
selectable是否可選中booleantrue
selectedKeys(受控)設(shè)置選中的樹節(jié)點(diǎn),多選需設(shè)置 multiple 為 truestring[]"-"
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--
stylestyle樣式{}--
childrenchildren節(jié)點(diǎn)ReactNode--
ref-TreeInstance--

以上就是使用React封裝一個Tree樹形組件的實例代碼的詳細(xì)內(nèi)容,更多關(guān)于React封裝Tree組件的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • react native帶索引的城市列表組件的實例代碼

    react native帶索引的城市列表組件的實例代碼

    本篇文章主要介紹了react-native城市列表組件的實例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • react 項目 中使用 Dllplugin 打包優(yōu)化技巧

    react 項目 中使用 Dllplugin 打包優(yōu)化技巧

    在用 Webpack 打包的時候,對于一些不經(jīng)常更新的第三方庫,比如 react,lodash,vue 我們希望能和自己的代碼分離開,這篇文章主要介紹了react 項目 中 使用 Dllplugin 打包優(yōu)化,需要的朋友可以參考下
    2023-01-01
  • React組件的用法概述

    React組件的用法概述

    React組件用來實現(xiàn)局部功能效果的代碼和資源的集合(html/css/js/image等等),這篇文章主要介紹了React組件的用法和理解,需要的朋友可以參考下
    2023-02-02
  • React超詳細(xì)講述Fiber的使用

    React超詳細(xì)講述Fiber的使用

    在fiber出現(xiàn)之前,react的架構(gòu)體系只有協(xié)調(diào)器reconciler和渲染器render。當(dāng)前有新的update時,react會遞歸所有的vdom節(jié)點(diǎn),如果dom節(jié)點(diǎn)過多,會導(dǎo)致其他事件影響滯后,造成卡頓。即之前的react版本無法中斷工作過程,一旦遞歸開始無法停留下來
    2023-02-02
  • react組件實例屬性props實例詳解

    react組件實例屬性props實例詳解

    這篇文章主要介紹了react組件實例屬性props,本文結(jié)合實例代碼給大家簡單介紹了props使用方法,代碼簡單易懂,需要的朋友可以參考下
    2023-01-01
  • 詳解React Fiber的工作原理

    詳解React Fiber的工作原理

    這篇文章主要介紹了React Fiber的工作原理的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用React框架,感興趣的朋友可以了解下
    2021-04-04
  • 詳解使用React.memo()來優(yōu)化函數(shù)組件的性能

    詳解使用React.memo()來優(yōu)化函數(shù)組件的性能

    本文講述了開發(fā)React應(yīng)用時如何使用shouldComponentUpdate生命周期函數(shù)以及PureComponent去避免類組件進(jìn)行無用的重渲染,以及如何使用最新的React.memo API去優(yōu)化函數(shù)組件的性能
    2019-03-03
  • React?SSG實現(xiàn)Demo詳解

    React?SSG實現(xiàn)Demo詳解

    這篇文章主要為大家介紹了React?SSG實現(xiàn)Demo詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>
    2023-07-07
  • react遞歸組件實現(xiàn)樹的示例詳解

    react遞歸組件實現(xiàn)樹的示例詳解

    在一些react項目中,常常有一些需要目錄樹這種結(jié)構(gòu),這篇文章主要為大家介紹了如何使用遞歸組件實現(xiàn)樹,感興趣的小伙伴可以了解下
    2024-10-10
  • JavaScript React如何修改默認(rèn)端口號方法詳解

    JavaScript React如何修改默認(rèn)端口號方法詳解

    這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號方法詳解,文中通過步驟圖片解析介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07

最新評論