[React Ocean 组件库] 实现交互式 | 可增删改查的树形控件

7,103 阅读3分钟

树形控件,交互展示

添加节点

add.gif

编辑节点

edit.gif

删除节点

delete.gif

灵感来自

树的结构灵感来自:Material UI源码。

地址:github.com/mui/materia…

交互灵感来自:Notability

地址:notability.com/zh-Hans

需求灵感来自:项目当中使用 Antd 的 TreeNode 来递归构造树

(事实上 AntdTreeNode 是 另外一个库 rc-tree

地址:www.npmjs.com/package/rc-…

需求

  • 实现字段高度可配置。(不一定是 children 可能是 childChapter……, 不一定是 id,可能是 chapterId&……)
  • 实现可交互式树的展开,收缩,以及节点交互。
  • 实现树节点的增删改查。
  • 实现预览模式和交互模式
  • 树的展开,收缩,选择全部受控。

第一:树基本使用

 <Tree mode="preview">
        <TreeItem label="第一章" id="1">
          <TreeItem label="第一章第一节" id="2">
            <TreeItem label="第一课时" id="3"></TreeItem>
          </TreeItem>
        </TreeItem>
        <TreeItem label="第二章" id="1">
          <TreeItem label="第二章第二节" id="2">
            <TreeItem label="第一课时" id="3"></TreeItem>
          </TreeItem>
        </TreeItem>        
 </Tree>

第二:实现树的基本结构

// TreeItem
const TreeItem = () => {
    return <TreeItemRoot>
       <TreeLabel onClick={(e) => clickTreeLabel(e)} isSelect={isSelect}/>
        {props.children && (
            <TransitionGroup>
              <Collapse ref={collapseRef}>{props.children}</Collapse>
            </TransitionGroup>
        )}
           </TreeItemRoot>
}

//Tree
const Tree = () => {
    <TreeContext.Provider value={{ expand, select, handleExpand, handleSelect, tool }}>
      <TreeContainer>{props.children}</TreeContainer>
    </TreeContext.Provider>
}

上述的代码结构,其实就是下面盒子的嵌套结构,去掉边框,就是一棵树结构。

image.png

第一:展开 收缩基本结构


 <TransitionGroup>
          <Collapse ref={collapseRef}>{props.children}</Collapse>
 </TransitionGroup>
        
        
 const Collapse = () => {
     return <CSSTransition
        in={expand}
        nodeRef={wrapperRef}
        timeout={330}
        classNames={'expand'}
        onEnter={handleOnEnter}
        onEntering={handleEntering}
        onEntered={handelEntered}
        onExit={handleOnExit}
        onExiting={handleOnExiting}
      >
        <TreeItemContainer ref={wrapperRef}>{props.children}</TreeItemContainer>
      </CSSTransition>
 }

上述的代码结构,让每一组 Collapse 成为了一个 CSS Transition

image.png

实现树的展开和收缩

第一:收缩的时候,高度变为 0 + 过渡

第二:扩张的时候,高度变为本来容器的高度 + 过渡

这里有个细节大家要注意:扩张结束之后,高度应该设为 auto, 不设 auto,树展开的高度计算就会有偏差,导致出现 UI bug

  const transitionCallback = (callback: callback) => () => {
    callback(wrapperRef.current, heightRef);
  };

    
  const handleOnEnter = transitionCallback((node: HTMLElement) => {
    node.style['height'] = collapsedSize + 'px';
  });

  const handleEntering = transitionCallback((node: HTMLElement) => {
    node.style['height'] = heightRef.current;
  });

  const handelEntered = transitionCallback(
    (node: HTMLElement, heightRef: React.MutableRefObject<string>) => {
      node.style['height'] = 'auto';
      heightRef.current = getWrapperHeight();
    },
  );

  const handleOnExit = transitionCallback(
    (node: HTMLElement, heightRef: React.MutableRefObject<string>) => {
      const height = node.clientHeight + 'px';
      node.style['height'] = height;

      if (!heightRef.current) {
        heightRef.current = height;
      }
    },
  );

  const handleOnExiting = transitionCallback((node: HTMLElement) => {
    wrapperRef.current.style['height'] = 0;
  });
  
  
   const Collapse = () => {
     return <CSSTransition
        in={expand}
        nodeRef={wrapperRef}
        timeout={330}
        classNames={'expand'}
        onEnter={handleOnEnter}
        onEntering={handleEntering}
        onEntered={handelEntered}
        onExit={handleOnExit}
        onExiting={handleOnExiting}
      >
        <TreeItemContainer ref={wrapperRef}>{props.children}</TreeItemContainer>
      </CSSTransition>
 }

实现节点的交互

分层

UI渲染层最底层的UI渲染层

const TreeUI = generateUI(treeData);
const Tree = () => {
    <Tree>{TreeUI}</Tree>
}

UI 驱动层UI 渲染层 的上层,也就是倒数第二层:UI 驱动层

怎么理解 UI 驱动? 就是把 数据一 这样的数据,通过递归变成数据二

数据一

  const [treeData, setTreeData] = useImmer<TreeData>([
    {
      anyName: '第一章',
      anyId: '0',
    },
    {
      anyName: '第二章',
      anyId: '1',
    },
  ]);

数据二

<TreeItem label="第一章" id="1"></TreeItem>;
<TreeItem label="第一章" id="1"></TreeItem>;

递归实现从数据一 到 数据二



  const generateUI = (treeData: TreeData) => {
    return treeData.map((item: TreeNode) => {
      if (item?.anyChild && item.anyChild?.length > 0) {
        return (
          <TreeItem>
            {generateUI(item.anyChild)}
          </TreeItem>
        );
      }

      if (item.anyId === curAddNode.anyId) {
        return interactState.addFocus ? (
          <TreeFocus/>
        ) : (
          <TreeItem />
        );
      }

      return (
        <TreeItem/>
      );
    });
  };

业务层

UI 驱动层上层,也就是倒数第二层:业务层

怎么理解业务层,笔者所做的业务就是知识点树的增删改查,章节学习树的增删改查,即前后端联调下的节点,也就是资源,课时,知识点 的增删改查,和节点的前端交互实现。

节点的交互是如何实现的?

第一:点击添加节点,创建一个节点,加入树当中去(推荐 immer)。

第二:遍历树节点之后,遍历到这个节点把他变为 focus 状态,也就是下面代码中的 <TreeFocus/>

第三:点击确定之后,取消 foces 转台,返回正常的树节点。

第四:点击取消之后,直接删掉这个节点就可以 (推荐 immer)

if (item.anyId === curAddNode.anyId) {
        return interactState.addFocus ? (
          <TreeFocus />
        ) : (
          <TreeItem/>
        );
      }

注意事项

上述实例只是一个简单的树形案例。实际在业务当中,情况要复杂非常之多。这里的小实例只是给大家一些灵感,笔者也希望可以得到大家的指正,笔者在业务当中的场景,可以参照该仓库的代码:github.com/Ryan-eng-de…

亟待改进

  • 传递 expandKeys 使得树的展开高度受控。
  • 传递 selectKeys 使得树的选择高度受控。
  • 从使用者的角度上优化代码 | 优化类型。
  • 增加可扩展工具栏。

期望

“不要走在我的后面,因为我可能不会引路;不要走在我的前面,因为我可能不会跟随;请走在我的身边,做我的朋友”

笔者,同时也真心希望可以找到一群,相互学习与鼓励,相互进步,相互欣赏的朋友。