React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React封装Tree组件

使用React封装一个Tree树形组件的实例代码

作者:滑动变滚动的蜗牛

这篇文章主要介绍了使用React封装一个Tree树形组件的实例,文中通过代码示例讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下

前言

为什么要造这样一个轮子呢?

最近在学习 next ,想用 next 重构一下自己的博客,而在 自己博客 的编辑页面中有使用到 antd 的一个树形的结构组件来展示文章的分类;

而我的 个人博客 (next版) 使用的是 next-ui ,但是里面并没有 tree 组件,看了下最近很火的 shadcn 也没有类似组件,我也不想为了 tree 又引入 antd 了,就想着自己封装一个玩玩,权当提升技术了(当然了非 next 版)。顺便还能为 我的组件库 添加一员。

当然我是对照 antd 作为模板开发的,但是他的 tree 是没有单独 check 的,当时我的旧版博客中为了实现该需求我可没少费工夫。

线上 Demo

源码

实现思路

我这里主要是根据 antdProps 选择一部分,并按照自身需求来增减实现的。

下面我就讲述整个 tree 树形组件的核心部分吧,其他一些属性就不细讲了,感兴趣可以直接看 源码

Html 基本结构

下面是整个组件的基本结构,renderTreeList 函数递归调用渲染 treechildren 节点。

类名 node-content 中的就是节点的内容了,根据需求样式自定义即可。

const Tree = forwardRef<TreeInstance, TreeProps>((props, ref) => {
  const { checkable, treeData, checkedKeys, defaultExpandAll, multiple, singleSelected, selectable = true, selectedKeys: propsSelectedKeys, onCheck, onSelect, onRightClick, ...ret } = props
  
  // ... 省略部分内容,只展示核心结构

  // 递归渲染 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>
  )
})

实现交互的树形结构 (checkTree)

生成一个用于实现交互效果的树形结构 ( checkTree )

export type CheckTreeItem = {
  /** 父节点的 key 值 */
  parentKey?: string
  /** 子节点的 key 数组 */
  childKeys?: string[]
  /** 是否展开 */
  show: boolean
  /** 是否选中 */
  checked: boolean
  /** 是否有 checkbox */
  checkable?: boolean
  /** 禁用 checkbox */
  disableCheckbox?: boolean
  /** 禁止整个节点的选择 */
  disabled?: boolean
}

export type CheckTree = Record<string, CheckTreeItem>
// ...
const [checkTree, setCheckTree] = useState<CheckTree>();

整体是一个只有一层结构的对象,使用每一项数据中唯一的 key 值作为 checkTree 的 key,通过 parentKey 和 childKeys 来查找该节点的 父子兄弟节点

例:

初始化树形结构

根据 generateCheckTree 函数的递归调用,将传入的 treeData 树状结构数据转变为组件需要的 checkTree

// ...
useEffect(() => {
  if(!treeData?.length) return
  const generateCheckTree = (list: TreeNode[], parentKey?: string) => {
    return list?.reduce((pre, cur) => {
      // checkedKeys 就是默认传入 check 项,用于默认是否勾选
      const curChecked = Boolean(checkedKeys?.includes(cur.key));
      pre[cur.key] = {
        // 默认是否展开该树形结构
        show: !!defaultExpandAll, 
        checked: curChecked, 
        parentKey,
      }
      // 一些属性的默认值
      if(cur.checkable) pre[cur.key].checkable = true
      if(cur.disableCheckbox) pre[cur.key].disableCheckbox = true
      if(cur.disabled) pre[cur.key].disabled = true
      // 有孩子节点就递归调用,生成数据
      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 转变为 checkTree

点击 check 节点

对应上面 html 结构中的 CheckBox 位置, checkable 等属性就是用来判断是否展示禁用 CheckBox 的。

// ...
{(checkable && item.checkable !== false) && (
  <CheckBox 
    checked={checkItem.checked} 
    disabled={item.disabled || item.disableCheckbox}
    // 先忽略,用来判断当前是否有孩子节点被选中了,true 则代表需要展示 checkbox 的半选样式
    indeterminate={getIsSomeChildCheck(checkItem, checkTree)}
    onChange={() => {
      if(item.disabled || item.disableCheckbox) return
      onNodeCheck(item.key)
    }} 
  />   
)}

先看 onChange 中触发的回调 onNodeCheck 函数,该函数主要是将 checkItem 中对应该项的 checked 取反一下。

/** 点击选中节点 */
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})

  // 先忽略,用来获取当前 check 的所有 key 值
  const keys = getCheckKeys(checkTree!)

  // check 触发的组件回调
  onCheck?.(keys, {
    key, 
    // 这步判断主要是单选时,选择父节点时只会选中其子节点
    checked: keys.includes(key) ? curChecked : false,  
    parentKeys: getParentKeys(key, checkTree!),
    treeDataItem: getTreeDataItem(key, treeData),
  })
}

然后通过 onCheckChildAndParent 函数,处理对应的父子节点的选中状态。

/** 处理父子节点的选中状态 */
const onCheckChildAndParent = (key: string, curChecked: boolean, cTree = checkTree!) => {
  const checkItem = cTree[key];

  // 全选/不选所有子节点
  (function checkAllChild(childKeys?: string[]) {
    childKeys?.forEach(childKey => {
      cTree[childKey].checked = curChecked
      checkAllChild(cTree[childKey].childKeys)
    })
  })(checkItem.childKeys);

  // 处理父节点的选中状态
  (function checkAllParent(parentKey?: string) {
    if(!parentKey) return
    if(!curChecked) { // 取消所有父节点的选中
      cTree[parentKey].checked = false
      checkAllParent(cTree[parentKey].parentKey)
    } else { // 将所有子节点被全选的父节点也选中
      const isSiblingCheck = !!cTree[parentKey].childKeys?.every(childKey => cTree[childKey].checked)
      if(isSiblingCheck) { // 判断兄弟节点是否也全被选中
        cTree[parentKey].checked = true
        checkAllParent(cTree[parentKey].parentKey)
      }
    }
  })(checkItem.parentKey);

  // 同层单选时,使兄弟节点取消选中
  if(singleSelected && curChecked) {
    const keys = cTree[key].parentKey ? cTree[cTree[key].parentKey!].childKeys : firstNodeKeys
    keys?.forEach(siblingKey => {
      if(siblingKey !== key) {
        cTree[siblingKey].checked = false
      }
    })
  }
}

子节点的展开实现

html 结构和 css 简单样式如下,通过 show 属性给 children 节点赋高度,由于定义了 transition 属性,所以当高度变化时,就会触发节点的 展开/收缩 动画。

<div 
  className={`node-children`} 
  // height: fit-content; 无法触发过渡效果,需要准确的值
  // 也可通过 maxHeight 设置一个很大的值来解决,但值过大又会使过度效果难看,所以这里需要获取一个准确的高度
  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;
}

这里有一个点要注意,就是无法直接给子节点定义一个由内容撑开的高度 height: fit-content;,这样会使 transition 无法正常触发。当然可以通过给一个比较大的 maxHeight 来设置最大高度,这样 transition 就会以 maxHeight 的高度实现动画效果,但是这样当子节点总高度和 maxHeight 出入过大时就会使动画效果很不好看。

所以我这里最终通过 getTreeChildHeight 函数来准确计算孩子节点的总高度了。

首先等待 checkTree 完成构建以及树形结构渲染完成,然后准确获取每个节点的高度,因为每个节点的 title 都是 ReactNode ,所以需要都获取一遍他们的高度。

/** 标题的最小高度 */
const TITLE_MIN_HEIGHT = 24;
/** 每个标题的下边距 */
const TITLE_MB = 6;

// 等待树形结构渲染完毕,获取 title 的高度
useEffect(() => {
  if(!checkTree || !isTreeRender.current) return
  const info: TitleNodeInfo = {};
  for(let key in checkTree) {
    // 每个标题渲染的内容,都要根据 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])

此时每个节点的 children 节点的高度,就能通过 getTreeChildHeight 函数递归计算得出了。

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 方法

然后我在组件里面实现了一些用于获取 treeData 数据的一些方法,简单来说都是一些递归调用等方法。

属性名描述类型
getCheckTree获取当前选中的树形结构() => CheckTree | undefined
getParentKeys根据 key 值获取其父节点,从 key 节点的最亲关系开始排列(key: string) => string[] | undefined
getSiblingKeys根据 key 值获取其兄弟节点,会包括自身节点(key: string) => string[] | undefined
getChildKeys根据 key 值获取其子节点(key: string) => string[] | undefined
getCheckKeys获取当前 check 中的所有 key() => string[]
getTreeDataItem获取当前 treeData 中的节点数据(key: string) => TreeNode | undefined

最终实现的 Props

其他属性的功能实现我就不一一叙述了,感兴趣可以直接看 源码

属性名描述类型默认值
checkable是否有选择框booleanfalse
checkedKeys(受控)选中复选框的树节点的key,当不在数组中的父节点需要被选中时,对应节点也将选中,触发 onCheck 回调,使该值保持正确string[]null
defaultExpandAll默认展开所有树节点booleanfalse
multiple支持点选多个节点(节点本身)booleanfalse
singleSelected是否只能单选一个节点booleanfalse
selectable是否可选中booleantrue
selectedKeys(受控)设置选中的树节点,多选需设置 multiple 为 truestring[]"-"
treeData树形结构的数据TreeNode[]--
onCheck点击复选框触发(checkedKeys: string[], params?: OnCheckParams) => void--
onSelect点击树节点触发(selectKeys: string[], params: OnSelectParams) => void--
onRightClick点击右键触发(params: onRightClickParams) => void--
className类名string--
stylestyle样式{}--
childrenchildren节点ReactNode--
ref-TreeInstance--

以上就是使用React封装一个Tree树形组件的实例代码的详细内容,更多关于React封装Tree组件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文