使用指南
安装
npm install he-tree-react
pnpm add he-tree-react
yarn add he-tree-react
数据类型
此库支持两种结构的数据:
- 扁平数据, 即一个一维数组. 类似与存储在数据库中的数据. 每项需要
id
, 父级 id,null
代表没有父级. 扁平数据的顺序必须跟树一样, 你可以在初始化数据时使用sortFlatData
方法给数据排序.js[ { id: 1, pid: null }, { id: 2, pid: 1 }, { id: 3, pid: 2 }, ];
- 树形数据. 使用
children
数组包含子节点. 如果未指定id
, 此库将使用节点在树中的索引作为id
. 使用树形数据时需设置dataType: 'tree'
.
[
{
id: 1,
children: [
{
id: 2,
children: [{ id: 3 }],
},
],
},
];
数据中的id, pid, children
不是固定的. 在设置中, 使用idKey, parentIdKey, childrenKey
表明你的数据中的对应键名.
没有组件
此库没有导出组件,而是导出一个 hook useHeTree
. 使用它返回的renderTree
渲染树. 这样做的好处是除了renderTree
, useHeTree
还会返回一些内部状态和方法, 可以轻松的被获取.
import { useHeTree } from "he-tree-react";
export default function App() {
const { renderTree } = useHeTree({...})
return <div>
{renderTree()}
</div>
}
选项
useHeTree
是主要使用的函数, 它的第一个参数是选项对象. 必须的选项有data
, 必须两者中有一个的是renderNode, renderNodeBox
. 其他重要选项是:
dataType
, 表明数据类型. 可用值:flat
, 默认. 扁平数据.tree
, 树形数据.
idKey, parentIdKey
, 默认值是id
和parent_id
. 使用扁平数据时需要. 虽然有默认值, 但还是建议写明更好.childrenKey
, 默认是children
. 使用树形数据时需要. 虽然有默认值, 但还是建议写明更好.onChange
, 数据改变时调用的函数, 参数是新数据. 如果你的树不会改变则不需要.isFunctionReactive
, 布尔. 默认false
.useHeTree
选项中包含许多回调函数, 如onChange, canDrop
.isFunctionReactive
可用来控制是否监听这些回调函数的改变. 如果你的回调函数和data
是同步改变的, 则不用启用此项. 否则你需要启用此项, 并且用 React 的useCallback
或useMemo
缓存你的所有回调函数以避免性能问题.
提示
stat
, 单个节点的相关信息. 大部分回调函数的参数里有stat
. 参考Stat
API.node
, 节点的数据. 通过stat.node
可以获取节点数据.getStat
, 通过此函数可以获取stat
, 唯一参数可以是id, node, stat
. 此函数在useHeTree
的返回对象中:const {getStat} = useHeTree({...})
.- 下面的代码例子附带有运行效果. 这些例子可以直接复制使用. 注意其中的高亮行的代码.
- 下面的代码例子使用
tsx
格式, 如果你需要js
格式, 可以使用任意 ts js 在线转换器.
基础使用-扁平数据
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
const [data, setdata] = useState(() => sortFlatData([
{
id: 1,
parent_id: null,
name: "Root Category",
},
{
id: 2,
parent_id: 1,
name: "Technology",
},
{
id: 5,
parent_id: 2,
name: "Hardware",
},
{
id: 10,
parent_id: 5,
name: "Computer Components",
},
{
id: 4,
parent_id: 2,
name: "Programming",
},
{
id: 8,
parent_id: 4,
name: "Python",
},
{
id: 3,
parent_id: 1,
name: "Science",
},
{
id: 7,
parent_id: 3,
name: "Biology",
},
{
id: 6,
parent_id: 3,
name: "Physics",
},
], keys));
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, open, checked }) => <div>
{node.name}
</div>,
})
return <div>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
基础使用-树形数据
import { useHeTree } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const [data, setdata] = useState(() => [
{
id: 1,
name: "Root Category",
children: [
{
id: 2,
name: "Technology",
children: [
{
id: 5,
name: "Hardware",
children: [
{
id: 10,
name: "Computer Components",
children: [],
},
],
},
{
id: 4,
name: "Programming",
children: [
{
id: 8,
name: "Python",
children: [],
},
],
},
],
},
{
id: 3,
name: "Science",
children: [
{
id: 7,
name: "Biology",
children: [],
},
{
id: 6,
name: "Physics",
children: [],
},
],
},
],
},
]);
const { renderTree } = useHeTree({
data,
dataType: 'tree',
childrenKey: 'children',
onChange: setdata,
renderNode: ({ id, node, open, checked }) => <div>
{node.name}
</div>,
})
return <div>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
自定义拖拽触发元素
给节点任意子元素添加draggable
属性即可.
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, open, checked, draggable }) => <div>
<button draggable={draggable}>Drag</button>
{node.name}
</div>,
})
return <div>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
节点 HTML 结构和样式
节点 HTML 如下:
<div
draggable="true"
data-key="1"
data-level="1"
data-node-box="true"
style="padding-left: 0px;"
>
<div>Node</div>
</div>
上面有两个 div. 使用renderNode
选项控制内层 div 的渲染. 如: renderNode: ({node}) => <div>{node.name}</div>
.
外层节点被称为nodeBox
, 不要修改它的padding-left, padding-right
. 使用选项indent
控制节点的缩进. 如果你想控制nodeBox
或拖拽占位节点的渲染, 可以使用renderNodeBox
选项, 这将覆盖renderNode
. 标准的renderNodeBox
如下:
renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
<div {...attrs} key={attrs.key}>
{isPlaceholder ? (
<div
className="he-tree-drag-placeholder"
style={{ minHeight: "20px", border: "1px dashed blue" }}
/>
) : (
<div>{/* node area */}</div>
)}
</div>
);
第 4 到第 7 行是拖拽占位节点. 第 9 行是节点元素.
自定义拖拽占位节点和 node box
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
<div {...attrs} key={attrs.key}>
{isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
: <div className="mynode">{stat.node.name}</div>
}
</div>
),
})
return <div>
{renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
<style>{`
.mytree [data-node-box]{
padding: 5px 0;
}
.mytree [data-node-box]:hover{
background-color: #eee;
}
.mytree .he-tree-drag-placeholder{
height: 30px;
line-height: 30px;
text-align: center;
border: 1px dashed red;
}
.mynode{
padding-left:5px;
}
`}</style>
</div>
}
节点的展开与折叠
- 使用选项
openIds
表明展开的节点. - 可通过
stat.open
获取该节点的open
状态. useHeTree
返回的allIds
包含所有节点的 id.- 此库导出了方法可以展开单个或多个节点的所有父级. 扁平数据:
openParentsInFlatData
. 树形数据:openParentsInTreeData
.
import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const [openIds, setopenIds] = useState<Id[] | undefined>([]);
const handleOpen = (id: Id, open: boolean) => {
if (open) {
setopenIds([...(openIds || allIds), id]);
} else {
setopenIds((openIds || allIds).filter((i) => i !== id));
}
}
const { renderTree, allIds } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
openIds,
renderNode: ({ id, node, open, checked, draggable }) => <div>
<button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
{node.name} - {id}
</div>,
})
return <div>
<button onClick={() => setopenIds(allIds)}>Open All</button>
<button onClick={() => setopenIds([])}>Close All</button>
<button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
<button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
此例子顶部 4 个按钮分别是: 展开全部, 折叠全部, 展开'Python'节点的所有父节点, 仅展开'Python'节点的所有父节点.
节点的勾选
- 使用选项
checkedIds
表明勾选的节点. - 可通过
stat.checked
获取该节点的checked
状态. - 此库导出了方法可以获取单个或多个节点
checked
变动后的checkedIds
. 扁平数据:updateCheckedInFlatData
. 树形数据: `updateCheckedInTreeData.- 此方法对节点的
checked
的更新是级联的. 如果你不想级联更新, 使用你自己的逻辑替代. - 此方法返回一个长度 2 的数组. 第一项是所有勾选的 id, 第二项是所有半选的 id. 如果不需要半选, 忽略第二项.
- 半选, 即同时有子节点被勾选或半选, 也有子节点未被勾选.
- 此方法对节点的
import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const [checkedIds, setcheckedIds] = useState<Id[]>([]);
const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
const handleChecked = (id: Id, checked: boolean) => {
const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
setcheckedIds(r[0]);
setsemiCheckedIds(r[1]);
}
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
checkedIds,
renderNode: ({ id, node, open, checked, draggable }) => <div>
<input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
{node.name} - {id}
</div>,
})
return <div>
Checked: {JSON.stringify(checkedIds)} <br />
Semi-Checked: {JSON.stringify(semiCheckedIds)}
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
控制是否可拖拽, 可放入
使用以下选项控制:
canDrag
, 节点是否可拖拽.canDrop
, 节点是否可放入.canDropToRoot
, 树根是否可放入.
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, open, checked, draggable }) => <div>
{node.name} - {id}
</div>,
canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
canDropToRoot: (index) => false,
})
return <div>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
- 根节点不可放入.
Technology
及子节点可以拖拽.Science
及子节点不可以拖拽.Science
及子节点可以放入.Technology
及子节点不可以放入.
拖拽到节点上时打开节点
使用以下选项控制:
dragOpen
, 是否启用, 默认false
.dragOpenDelay
, 延时, 默认600
毫秒.onDragOpen
, 打开节点时调用的函数.
import { useHeTree, sortFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
const handleOpen = (id: Id, open: boolean) => {
if (open) {
setopenIds([...(openIds || allIds), id]);
} else {
setopenIds((openIds || allIds).filter((i) => i !== id));
}
}
const { renderTree, allIds } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
openIds,
renderNode: ({ id, node, open, checked, draggable }) => <div>
<button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
{node.name} - {id}
</div>,
dragOpen: true,
onDragOpen(stat) {
handleOpen(stat.id, true)
},
})
return <div>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
更新数据
由于 React 的不可变特性, 扁平数据和树形数据更新都很困难. 针对扁平数据, 此库提供了两个方法, 用以增加节点或删除节点. 如果你要进行更复杂的操作, 或者更新树形数据, 推荐你使用immer
.
npm install immer use-immer
pnpm add immer use-immer
yarn add immer use-immer
使用内置方法更新扁平数据
addToFlatData
: 增加节点. removeByIdInFlatData
: 删除节点. 这两个方法都会改变原数据, 所以把原数据的复制传给它, 或者与immer
一起使用.
import {
useHeTree, sortFlatData,
addToFlatData, removeByIdInFlatData
} from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef, useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const add = (pid: Id) => {
let id = parseInt(Math.random().toString().substring(2, 5));
let newData = [...data];
addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
setdata(newData);
}
const remove = (id: Id) => {
let newData = [...data];
removeByIdInFlatData(newData, id as number, keys)
setdata(newData);
}
const initialData = useRef<typeof data>();
initialData.current = initialData.current || data;
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, draggable }) => <div>
<button draggable={draggable}>👉</button>
{node.name} - {id} -
<button onClick={() => add(id)}>+</button>
<button onClick={() => remove(id)}>-</button>
</div>,
})
return <div>
<button onClick={() => setdata(initialData.current!)}>Restore</button>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
使用 immer 更新扁平数据
注意, 这里使用了useImmer
替代 React 的useState
.
import {
useHeTree, sortFlatData,
addToFlatData, removeByIdInFlatData
} from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef } from 'react';
import { useImmer } from "use-immer";
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const add = (pid: Id) => {
let id = parseInt(Math.random().toString().substring(2, 5));
setdata(draft => {
addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
});
}
const remove = (id: Id) => {
setdata(draft => {
removeByIdInFlatData(draft, id as number, keys)
})
}
const edit = (id: Id) => {
let newName = prompt("Enter new name")
setdata(draft => {
if (newName) {
draft.find(node => node.id === id)!.name = newName
}
})
}
const initialData = useRef<typeof data>();
initialData.current = initialData.current || data;
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, draggable }) => <div>
<button draggable={draggable}>👉</button>
{node.name} - {id} -
<button onClick={() => add(id)}>+</button>
<button onClick={() => remove(id)}>-</button>
<button onClick={() => edit(id)}>Edit</button>
</div>,
})
return <div>
<button onClick={() => setdata(initialData.current!)}>Restore</button>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
使用 immer 更新树形数据
注意, 这里使用了useImmer
替代 React 的useState
. findTreeData
方法类似数组的find
方法.
import { useHeTree, findTreeData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef } from 'react';
import { useImmer } from "use-immer";
export default function BasePage() {
const CHILDREN = 'children'
const keys = { idKey: 'id', childrenKey: CHILDREN };
// prettier-ignore
const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
const add = (pid: Id) => {
let id = parseInt(Math.random().toString().substring(2, 5));
setdata(draft => {
findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
})
}
const remove = (id: Id, pid: Id | null) => {
setdata(draft => {
const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
children.splice(children.findIndex(t => t.id === id), 1)
})
}
const edit = (id: Id) => {
let newName = prompt("Enter new name")
setdata(draft => {
if (newName) {
findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
}
})
}
const initialData = useRef<typeof data>();
initialData.current = initialData.current || data;
const { renderTree } = useHeTree({
...keys,
data,
dataType: 'tree',
onChange: setdata,
renderNode: ({ id, pid, node, draggable }) => <div>
<button draggable={draggable}>👉</button>
{node.name} - {id} -
<button onClick={() => add(id)}>+</button>
<button onClick={() => remove(id, pid)}>-</button>
<button onClick={() => edit(id)}>Edit</button>
</div>,
})
return <div>
<button onClick={() => setdata(initialData.current!)}>Restore</button>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
从外部发起的拖拽
相关选项:
onExternalDragOver
: 表明是否处理外部拖拽.onExternalDrop
: 当外部拖拽放入树中时调用的回调函数.
import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
import { useImmer } from "use-immer";
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'parent_id' };
// prettier-ignore
const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
const { renderTree, allIds } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
renderNode: ({ id, node, open, checked, draggable }) => <div>
{node.name} - {id}
</div>,
onExternalDragOver: (e) => true,
onExternalDrop: (e, parentStat, index) => {
setdata(draft => {
const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
addToFlatData(draft, newNode, index, keys)
})
},
})
return <div>
<button draggable={true}>Drag me in to the tree</button>
{renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
超大数据
使用选项virtual
启用虚拟列表功能. 记得给树设置可见区域高度.
import { useHeTree, sortFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';
export default function BasePage() {
const keys = { idKey: 'id', parentIdKey: 'pid' };
// prettier-ignore
const [data, setdata] = useState(() => sortFlatData(createData(), keys));
const [openIds, setopenIds] = useState<Id[] | undefined>([]);
const handleOpen = (id: Id, open: boolean) => {
if (open) {
setopenIds([...(openIds || allIds), id]);
} else {
setopenIds((openIds || allIds).filter((i) => i !== id));
}
}
const { renderTree, allIds } = useHeTree({
...keys,
data,
dataType: 'flat',
onChange: setdata,
openIds,
virtual: true,
renderNode: ({ id, node, open, checked, draggable }) => <div>
<button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
{id}
</div>,
})
return <div>
{renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
</div>
}
// generate 10000 nodes
function createData() {
const genId = () => result.length
const result: { id: number, pid: number | null }[] = [];
for (let i = 0; i < 1000; i++) {
let id1 = genId()
result.push({ id: id1, pid: null })
for (let j = 0; j < 4; j++) {
result.push({ id: genId(), pid: id1 })
}
let id2 = genId()
result.push({ id: id2, pid: null })
for (let j = 0; j < 4; j++) {
result.push({ id: genId(), pid: id2 })
}
}
return result;
}
触摸 & 移动设备
此库基于 HTML5 Drag and Drop API, 所以在支持 Drag and Drop API 的移动设备上能工作. 如果不支持, 可以尝试添加兼容 Drag and Drop API 的库.
注意
触摸时, 用户需要触摸并等一会儿才能触发拖拽。
其他
- 选项
direction
: 从右往左显示. - 选项
customDragImage
: 自定义 drag image. - 选项
rootId
: 使用扁平数据时, 顶级节点的父 id. - 选项
keepPlaceholder
: 拖拽到树外时, 是否要保留拖拽占位节点. 默认false
. - 辅助方法
scrollToNode
: 滚动到指定节点.