树形控件,交互展示
添加节点
编辑节点
删除节点
灵感来自
树的结构灵感来自:Material UI源码。
交互灵感来自:Notability。
需求灵感来自:项目当中使用 Antd 的 TreeNode 来递归构造树
(事实上 Antd 的 TreeNode 是 另外一个库 rc-tree)
需求
- 实现字段高度可配置。(不一定是 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>
}
上述的代码结构,其实就是下面盒子的嵌套结构,去掉边框,就是一棵树结构。
第一:展开 收缩基本结构
<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
实现树的展开和收缩
第一:收缩的时候,高度变为 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 使得树的选择高度受控。
- 从使用者的角度上优化代码 | 优化类型。
- 增加可扩展工具栏。
期望
“不要走在我的后面,因为我可能不会引路;不要走在我的前面,因为我可能不会跟随;请走在我的身边,做我的朋友”
笔者,同时也真心希望可以找到一群,相互学习与鼓励,相互进步,相互欣赏的朋友。