Skip to content

使用指南

安装

sh
npm install he-tree-react
sh
pnpm add he-tree-react
sh
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'.
js
[
  {
    id: 1,
    children: [
      {
        id: 2,
        children: [{ id: 3 }],
      },
    ],
  },
];

数据中的id, pid, children不是固定的. 在设置中, 使用idKey, parentIdKey, childrenKey表明你的数据中的对应键名.

没有组件

此库没有导出组件,而是导出一个 hook useHeTree. 使用它返回的renderTree渲染树. 这样做的好处是除了renderTree, useHeTree还会返回一些内部状态和方法, 可以轻松的被获取.

js
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, 默认值是idparent_id. 使用扁平数据时需要. 虽然有默认值, 但还是建议写明更好.
  • childrenKey, 默认是children. 使用树形数据时需要. 虽然有默认值, 但还是建议写明更好.
  • onChange, 数据改变时调用的函数, 参数是新数据. 如果你的树不会改变则不需要.
  • isFunctionReactive, 布尔. 默认false. useHeTree选项中包含许多回调函数, 如onChange, canDrop. isFunctionReactive可用来控制是否监听这些回调函数的改变. 如果你的回调函数和data是同步改变的, 则不用启用此项. 否则你需要启用此项, 并且用 React 的useCallbackuseMemo缓存你的所有回调函数以避免性能问题.

查看useHeTree的 API 文档以了解更多.

提示

  • stat, 单个节点的相关信息. 大部分回调函数的参数里有stat. 参考Stat API.
  • node, 节点的数据. 通过stat.node可以获取节点数据.
  • getStat, 通过此函数可以获取stat, 唯一参数可以是id, node, stat. 此函数在useHeTree的返回对象中: const {getStat} = useHeTree({...}).
  • 下面的代码例子附带有运行效果. 这些例子可以直接复制使用. 注意其中的高亮行的代码.
  • 下面的代码例子使用tsx格式, 如果你需要js格式, 可以使用任意 ts js 在线转换器.

基础使用-扁平数据

tsx
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>
}
请看效果

基础使用-树形数据

tsx
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属性即可.

tsx
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 如下:

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如下:

tsx
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

tsx
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.
tsx
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. 如果不需要半选, 忽略第二项.
    • 半选, 即同时有子节点被勾选或半选, 也有子节点未被勾选.
tsx
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>
}
请看效果

控制是否可拖拽, 可放入

使用以下选项控制:

tsx
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及子节点不可以放入.

拖拽到节点上时打开节点

使用以下选项控制:

tsx
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.

sh
npm install immer use-immer
sh
pnpm add immer use-immer
sh
yarn add immer use-immer

使用内置方法更新扁平数据

addToFlatData: 增加节点. removeByIdInFlatData: 删除节点. 这两个方法都会改变原数据, 所以把原数据的复制传给它, 或者与immer一起使用.

tsx
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.

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

tsx
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>
}
请看效果

从外部发起的拖拽

相关选项:

tsx
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启用虚拟列表功能. 记得给树设置可见区域高度.

tsx
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 的库.

注意

触摸时, 用户需要触摸并等一会儿才能触发拖拽。

其他

Last updated: