this.onExpand(v)}
expandedKeys={this.state.expandedKeys}
style={style}
/>
>
);
}
}
```
### 虚拟化
列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。
`virtualize` 是一个包含下列值的对象:
- height: 高度值,如果为 string 必须有计算高度才能被渲染出来,即其父节点有 offsetHeight
- width: 宽度值,默认 100%
- itemSize: 每行的treeNode的高度,必传
如果带搜索框,建议开启 `showFilteredOnly` 减少多余节点的渲染。
```jsx live=true hideInDSM
import React from 'react';
import { Tree, Button } from '@douyinfe/semi-ui';
class Demo extends React.Component {
constructor() {
super();
this.state = {
gData: [],
total: 0,
};
this.onGen = this.onGen.bind(this);
}
generateData(x = 5, y = 4, z = 3, gData = []) {
// x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级)
function _loop(_level, _preKey, _tns) {
const preKey = _preKey || '0';
const tns = _tns || gData;
const children = [];
for (let i = 0; i < x; i++) {
const key = `${preKey}-${i}`;
tns.push({ label: `${key}-标签`, key: `${key}-key`, value: `${key}-value` });
if (i < y) {
children.push(key);
}
}
if (_level < 0) {
return tns;
}
const __level = _level - 1;
children.forEach((key, index) => {
tns[index].children = [];
return _loop(__level, key, tns[index].children);
});
return null;
}
_loop(z);
function calcTotal(x, y, z) {
const rec = n => (n >= 0 ? x * y ** n-- + rec(n) : 0);
return rec(z + 1);
}
return { gData, total: calcTotal(x, y, z) };
}
onGen() {
const { gData, total } = this.generateData();
this.setState({
gData,
total
});
};
render() {
const style = {
width: 260,
border: '1px solid var(--semi-color-border)'
};
return (
共 {this.state.total} 个节点
{this.state.gData.length ? (
) : null}
);
}
}
```
### 动态更新数据
```jsx live=true hideInDSM
import React from 'react';
import { Tree, Button } from '@douyinfe/semi-ui';
class Demo extends React.Component {
constructor() {
super();
this.state = {
treeData: [{
key: '0',
label: 'item-0',
value: '0'
}],
};
this.add = this.add.bind(this);
}
add() {
const itemLength = Math.floor(Math.random() * 5) + 1;
const treeData = new Array(itemLength).fill(0).map((v, i) => {
const length = Math.floor(Math.random() * 3);
const children = new Array(length).fill(0).map((cv, ci) => {
const child = {
key: `${i}-${ci}`,
label: `Leaf-${i}-${ci}`,
value: `${i}-${ci}`
};
return child;
});
const item = {
key: `${i}`,
label: `Item-${i}`,
value: `${i}`,
children
};
return item;
});
this.setState({ treeData });
}
render() {
const { treeData } = this.state;
const style = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)'
};
return (
);
}
}
```
### 异步加载数据
通过设置 loadData 可以动态加载数据,此时需要在数据中传入 isLeaf 标明叶子节点。
```jsx live=true hideInDSM
import React, { useState } from 'react';
import { Tree } from '@douyinfe/semi-ui';
() => {
const initialData = [
{
label: 'Expand to load',
value: '0',
key: '0',
},
{
label: 'Expand to load',
value: '1',
key: '1',
},
{
label: 'Leaf Node',
value: '2',
key: '2',
isLeaf: true,
},
];
const [treeData, setTreeData] = useState(initialData);
function updateTreeData(list, key, children) {
return list.map(node => {
if (node.key === key) {
return { ...node, children };
}
if (node.children) {
return { ...node, children: updateTreeData(node.children, key, children) };
}
return node;
});
}
function onLoadData({ key, children }) {
return new Promise(resolve => {
if (children) {
resolve();
return;
}
setTimeout(() => {
setTreeData(origin =>
updateTreeData(origin, key, [
{
label: 'Child Node',
key: `${key}-0`,
},
{
label: 'Child Node',
key: `${key}-1`,
},
]),
);
resolve();
}, 1000);
});
}
return (
);
};
```
### 可拖拽的Tree
**v>=1.8.0**
通过设置 draggable 配合 onDrop 可以实现 Tree 节点的拖拽。
**目前不支持虚拟化**
```jsx live=true hideInDSM
import React, { useState } from 'react';
import { Tree } from '@douyinfe/semi-ui';
() => {
const initialData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0'
}
]
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0'
},
{
label: 'Canada',
value: 'Canada',
key: '1-1'
}
]
},
{
label: 'Europe',
value: 'Europe',
key: '2',
}
];
const [treeData, setTreeData] = useState(initialData);
function onDrop(info) {
const { dropToGap, node, dragNode } = info;
const dropKey = node.key;
const dragKey = dragNode.key;
const dropPos = node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = [...treeData];
const loop = (data, key, callback) => {
data.forEach((item, ind, arr) => {
if (item.key === key) return callback(item, ind, arr);
if (item.children) return loop(item.children, key, callback);
});
};
let dragObj;
loop(data, dragKey, (item, ind, arr) => {
arr.splice(ind, 1);
dragObj = item;
});
if (!dropToGap) {
// inset into the dropPosition
loop(data, dropKey, (item, ind, arr) => {
item.children = item.children || [];
item.children.push(dragObj);
});
} else if (dropPosition === 1 && node.children && node.expanded) {
// has children && expanded and drop into the node bottom gap
// insert to the top
loop(data, dropKey, item => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else {
let dropNodeInd;
let dropNodePosArr;
loop(data, dropKey, (item, ind, arr) => {
dropNodePosArr = arr;
dropNodeInd = ind;
});
if (dropPosition === -1) {
// insert to top
dropNodePosArr.splice(dropNodeInd, 0, dragObj);
} else {
// insert to bottom
dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
}
}
setTreeData(data);
}
return ;
};
```
### 高级定制
**版本 v>=1.7.0**
Tree 组件的 api 支持了大部分的渲染需求,但是如果有非常特殊的定制要求的话,可以使用 `renderFullLabel` 来接管整行 option 的渲染效果。
如果开启了虚拟化,需要将 style (虚拟化相关样式)赋予给渲染的 DOM 节点
这里给几个常见的高级用法的 demo。
第一个是针对 “希望只有叶子节点可以选中,父节点只起到分组作用” 的场景。
- 你只需要渲染叶子节点前的 Checkbox,并且点击父节点时不触发选中,点击叶子节点触发。
- 同时开启 leafOnly 可以使 onChange 的回调入参都是叶子节点。
⚠️:renderFullLabel 只接管了渲染效果,并不影响内部的数据逻辑。但是你可以选取需要的逻辑进行渲染,或者配合受控来实现更复杂的需求。
```jsx live=true
import React from 'react';
import { Tree, Checkbox } from '@douyinfe/semi-ui';
() => {
const renderLabel = ({
className,
onExpand,
data,
onCheck,
checkStatus,
expandIcon,
}) => {
const { label } = data;
const isLeaf = !(data.children && data.children.length);
return (
{isLeaf ? null : expandIcon}
{isLeaf ?
: null}
{label}
);
};
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0'
}
]
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0'
},
{
label: 'Canada',
value: 'Canada',
key: '1-1'
}
]
}
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)'
};
return (
);
};
```
第二个是针对 “希望只有叶子节点可以单选,父节点只起到分组作用” 的场景。
- 你只需要点击父节点时不触发选中,点击叶子节点触发。
```jsx live=true
import React from 'react';
import { Tree } from '@douyinfe/semi-ui';
() => {
const renderLabel = ({
className,
onExpand,
onClick,
data,
expandIcon,
}) => {
const { label } = data;
const isLeaf = !(data.children && data.children.length);
return (
{isLeaf ? null : expandIcon}
{label}
);
};
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{
label: 'Beijing',
value: 'Beijing',
key: '0-0-0',
},
{
label: 'Shanghai',
value: 'Shanghai',
key: '0-0-1',
},
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [
{
label: 'Osaka',
value: 'Osaka',
key: '0-1-0'
}
]
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{
label: 'United States',
value: 'United States',
key: '1-0'
},
{
label: 'Canada',
value: 'Canada',
key: '1-1'
}
]
}
];
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)'
};
return (
console.log('change', ...args)}
/>
);
};
```
第三个是针对 “单选选中父节点同时也高亮子节点” 的场景。
```jsx live=true
import React, { useState } from 'react';
import { Tree } from '@douyinfe/semi-ui';
import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSectionStroked, IconComponentStroked } from '@douyinfe/semi-icons';
() => {
const [selected, setSelected] = useState(new Set());
const [selectedThroughParent, setSelectedThroughParent] = useState(new Set());
const treeData = [
{
label: '黑色固定按钮',
icon: ,
key: 'fix-btn-0'
},
{
label: '模块',
key: 'module-0',
icon: ,
children: [
{
label: '可自由摆放的组件',
icon: ,
key: 'free-compo-0',
},
{
label: '分栏容器',
icon: ,
key: 'split-col-0',
children: [
{
label: '按钮组件',
icon: ,
key: 'btn-0'
},
{
label: '按钮组件',
icon: ,
key: 'btn-1'
}
]
},
],
},
{
label: '模块',
icon: ,
key: 'module-1',
children: [
{
label: '自定义组件',
icon: ,
key: 'cus-0'
}
]
}
];
const findDescendantKeys = (node) => {
let res = [node.key];
const findChild = item => {
if (!item) return;
const { children } = item;
if (children && children.length) {
children.forEach(child => {
res.push(child.key);
findChild(child);
});
}
};
findChild(node);
return res;
};
const handleSelect = (key, bool, node) => {
setSelected(new Set([key]));
const descendantKeys = findDescendantKeys(node);
setSelectedThroughParent(new Set(descendantKeys));
};
const renderLabel = ({
className,
data,
onClick,
expandIcon
}) => {
const { label, icon, key } = data;
const isLeaf = !(data.children && data.children.length);
const style = {
backgroundColor: selected.has(key)
? 'rgba(var(--semi-blue-0), 1)'
: selectedThroughParent.has(key)
? 'rgba(var(--semi-blue-0), .5)' : 'transparent'
};
return (
{isLeaf ? : expandIcon}
{icon}
{label}
);
};
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)'
};
return (
);
};
```
### 可拖拽的高级定制
我们从 1.27.0 版本开始支持可拖拽(`draggable`)和高级定制(`renderFullLabel`)同时使用,在该版本之前,并不支持二者同时使用。
```jsx live=true
import React, { useState } from 'react';
import { Tree } from '@douyinfe/semi-ui';
import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSectionStroked, IconComponentStroked } from '@douyinfe/semi-icons';
() => {
const [selected, setSelected] = useState(new Set());
const [selectedThroughParent, setSelectedThroughParent] = useState(new Set());
const defaultTreeData = [
{
label: '黑色固定按钮',
icon: ,
key: 'fix-btn-0'
},
{
label: '模块',
key: 'module-0',
icon: ,
children: [
{
label: '可自由摆放的组件',
icon: ,
key: 'free-compo-0',
},
{
label: '分栏容器',
icon: ,
key: 'split-col-0',
children: [
{
label: '按钮组件',
icon: ,
key: 'btn-0'
},
{
label: '按钮组件',
icon: ,
key: 'btn-1'
}
]
},
],
},
{
label: '模块',
icon: ,
key: 'module-1',
children: [
{
label: '自定义组件',
icon: ,
key: 'cus-0'
}
]
}
];
const [treeData, setTreeData] = useState(defaultTreeData);
const onDrop = (info) => {
const { dropToGap, node, dragNode } = info;
const dropKey = node.key;
const dragKey = dragNode.key;
const dropPos = node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = [...treeData];
const loop = (data, key, callback) => {
data.forEach((item, ind, arr) => {
if (item.key === key) return callback(item, ind, arr);
if (item.children) return loop(item.children, key, callback);
});
};
let dragObj;
loop(data, dragKey, (item, ind, arr) => {
arr.splice(ind, 1);
dragObj = item;
});
if (!dropToGap) {
loop(data, dropKey, (item, ind, arr) => {
item.children = item.children || [];
item.children.push(dragObj);
});
} else if (dropPosition === 1 && node.children && node.expanded) {
loop(data, dropKey, item => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else {
let dropNodeInd;
let dropNodePosArr;
loop(data, dropKey, (item, ind, arr) => {
dropNodePosArr = arr;
dropNodeInd = ind;
});
if (dropPosition === -1) {
dropNodePosArr.splice(dropNodeInd, 0, dragObj);
} else {
dropNodePosArr.splice(dropNodeInd + 1, 0, dragObj);
}
}
setTreeData(data);
};
const findDescendantKeys = (node) => {
const res = [node.key];
const findChild = item => {
if (!item) return;
const { children } = item;
if (children && children.length) {
children.forEach(child => {
res.push(child.key);
findChild(child);
});
}
};
findChild(node);
return res;
};
const handleSelect = (key, bool, node) => {
setSelected(new Set([key]));
const descendantKeys = findDescendantKeys(node);
setSelectedThroughParent(new Set(descendantKeys));
};
const renderLabel = ({
className,
data,
onClick,
expandIcon
}) => {
const { label, icon, key } = data;
const isLeaf = !(data.children && data.children.length);
const style = {
backgroundColor: selected.has(key)
? 'rgba(var(--semi-blue-0), 1)'
: selectedThroughParent.has(key)
? 'rgba(var(--semi-blue-0), .5)' : 'transparent'
};
return (
{isLeaf ? : expandIcon}
{icon}
{label}
);
};
const treeStyle = {
width: 260,
height: 420,
border: '1px solid var(--semi-color-border)'
};
return ;
};
```
## API参考
### Tree
| 属性 | 说明 | 类型 | 默认值 |版本 |
|------------- | ----------- | -------------- | -------------- | --------|
| autoExpandParent | 是否自动展开父节点,默认为 false,当组件初次挂载时为 true | boolean | false | 0.34.0 |
| autoExpandWhenDragEnter | 是否允许拖拽到节点上时自动展开改节点 | boolean | true | 1.8.0 |
| blockNode | 行显示节点 | boolean | true | - |
| className | 类名 | string | - | - |
| defaultExpandAll | 设置在初始化时是否展开所有节点。而如果后续数据(`treeData`/`treeDataSimpleJson`)发生改变,这个 api 是无法影响节点的默认展开情况的,如果有这个需要可以使用 `expandAll` | boolean | false | - |
| defaultExpandedKeys | 默认展开的节点,显示其直接子级 | string\[] | - | - |
| defaultValue | 指定默认选中的条目 | string \| number \| TreeNode \| (string \| number \| TreeNode)[] | - | - |
| directory | 目录树模式 | boolean | false | - |
| disableStrictly | 当节点的disabled状态确定时,不可通过子级或者父级的关系选中 | boolean | false | 1.4.0 |
| disabled | 禁用整个树,不可选择 | boolean | false | 0.32.0 |
| draggable | 是否允许拖拽 | boolean | false | 1.8.0 |
| emptyContent | 当搜索无结果时展示的内容 | ReactNode | `暂无数据` | 0.32.0 |
| expandAction | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开 | boolean \| string | false | 0.35.0 |
| expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`/`treeDataSimpleJson`)发生改变,默认展开情况也是会受到这个 api 影响的 | boolean | false | 1.30.0 |
| expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级 | string[] | - | - |
| filterTreeNode | 是否根据输入项进行筛选,默认用 `treeNodeFilterProp` 的值作为要筛选的 `TreeNode` 的属性值 | boolean \| ((inputValue: string, treeNodeString: string) => boolean) | false | - |
| hideDraggingNode | 是否隐藏正在拖拽的节点的 dragImg | boolean | false | 1.8.0 |
| icon | 自定义图标 | ReactNode | - | - |
| labelEllipsis | 是否开启label的超出省略,默认虚拟化状态开启,如果有其他省略需求可以设置关闭 | boolean | false\|true(virtualized) | 1.8.0 |
| leafOnly | 多选模式下是否开启 onChange 回调入参及展示标签只有叶子节点 | boolean | false | 1.18.0 |
| loadData | 异步加载数据,需要返回一个Promise | (treeNode?: TreeNode) => Promise< void > |- | 1.0.0|
| loadedKeys | (受控)已经加载的节点,配合 loadData 使用 | string[] | - | 1.0.0|
| motion | 是否开启动画 | boolean | true | - |
| multiple | 是否支持多选 | boolean | false | - |
| renderDraggingNode | 自定义正在拖拽节点的 dragImg 的 Html 元素 | (nodeInstance: HTMLElement, node: TreeNode) => HTMLElement | - | 1.8.0 |
| renderFullLabel | 完全自定义label的渲染函数 | (data: object) => ReactNode | - | 1.7.0 |
| renderLabel | 自定义label的渲染函数 | (label: ReactNode, data: TreeNode) => ReactNode |- | 1.6.0 |
| searchClassName | 搜索框的 `className` 属性 | string | - | - |
| searchPlaceholder | 搜索框默认文字 | string | - | - |
| searchRender | 自定义搜索框的渲染方法,为 false 时可以隐藏组件的搜索框(**V>=1.0.0**) | ((searchRenderProps: object) => ReactNode) \| false | - | 0.35.0 |
| searchStyle | 搜索框的样式 | CSSProperties | - | - |
| showClear | 支持清除搜索框 | boolean | true | 0.35.0 |
| showFilteredOnly | 搜索状态下是否只展示过滤后的结果 | boolean | false | 0.32.0 |
| style | 样式 | CSSProperties | - | - |
| treeData | treeNodes 数据,如果设置则不需要手动构造 `TreeNode` 节点(key值在整个树范围内唯一) | TreeNode[] | \[] | - |
| treeDataSimpleJson | 简单 JSON 形式的 `treeNodes` 数据,如果设置则不需要手动构造 TreeNode 节点,返回值为包含选中节点的Json数据 | TreeDataSimpleJson | \{} | - |
| treeNodeFilterProp | 搜索时输入项过滤对应的 `treeNode` 属性 | string | `label` | - |
| value | 当前选中的节点的value值,传入该值时将作为受控组件 | string \| number \| TreeNode \| (string \| number \| TreeNode)[] | - | - |
| virtualize | 列表虚拟化,用于大量树节点的情况,由 height, width, itemSize 组成,参考 Virtualize Object。开启后将关闭动画效果。 | VirtualizeObj | - | 0.32.0 |
| onChange | 选中树节点时调用此函数,默认返回值为当前所有选中项的value值 | (value?: string \| number \| TreeNode \| (string \| number \| TreeNode)[]) => void | - | - |
| onChangeWithObject | 是否将选中项 option 的其他属性作为回调。设为 true 时,onChange 的入参类型会从 string 变为 object: { value, label, ...rest }。此时如果是受控,也需要把 value 设置成 object,且必须含有 value 的键值;defaultValue同理。 | boolean | false | - |
| onDoubleClick | 双击事件的回调 | (e: MouseEvent, node: TreeNode) => void | - | 0.35.0 |
| onDragEnd | onDragEnd 事件回调 | (dragProps: object) => void | - | 1.8.0 |
| onDragEnter | onDragEnter 事件回调 | (dragEnterProps: object) => void | - | 1.8.0 |
| onDragLeave | onDragLeave 事件回调 | (dragProps: object) => void | - | 1.8.0 |
| onDragOver | onDragOver 事件回调 | (dragProps: object) => void | - | 1.8.0 |
| onDragStart | onDragStart 事件回调 | (dragProps: object) => void | - | 1.8.0 |
| onDrop | onDrop 事件回调 | (onDragProps: object) => void | - | 1.8.0 |
| onExpand | 展示节点时调用 | (expandedKeys: string[], {expanded: boolean, node: TreeNode}) => void | - | - |
| onLoad | 节点加载完毕时触发的回调 | (loadedKeys: Set, treeNode: TreeNode) => void |- | 1.0.0|
| onContextMenu | 右键点击的回调 | (e: MouseEvent, node: TreeNode) => void | - | 0.35.0 |
| onSearch | 文本框值变化时回调 | (sunInput: string) => void | - | - |
| onSelect | 被选中时调用,返回值为当前事件选项的key值 | (selectedKey:string, selected: bool, selectedNode: TreeNode) => void | - | - |
### TreeNode
> __不同 `TreeNode` 的 key 值要求必填且唯一。__`label` 允许重复。**v>=1.7.0** 之前 value 值要求必须必填且唯一。
> **v>=1.7.0** 之后 value 值非必填。此时 onChange, value, defaultValue 及 onChangeWithObject 中所取的 value 属性值将改为 key 值。
> 为了保证行为的符合预期,treeData 中的 value 值或者全部不填写,或者全部填写且唯一,不建议混写。
| 属性 | 说明 | 类型 | 默认值 |
|------------- | ----------- | -------------- | -------------- |
| value | 属性值 | string\|number | - |
| label | 展示的文本 | string\|ReactNode | - |
| icon | 自定义图标 | ReactNode | - |
| disabled | 是否禁用 | boolean | false |
| key | required且要求唯一 | string | - |
| isLeaf | 设置节点为叶子节点,在异步加载数据的情况即传入 loadData 时有效 **v>=1.0.0**| boolean | - |
### Virtualize Object
> `itemSize` 必传。
| 属性 | 说明 | 类型 | 默认值 |
|------------- | ----------- | -------------- | -------------- |
| height | 高度值,如果为 string 必须保证有计算高度,即其父节点有 offsetHeight | number\|string | '100%' |
| itemSize | 每行的treeNode的高度,必传 | number | - |
| width | 宽度值 | number\|string | '100%' |
## 设计变量
### Ref 方法
- search(sugInput) => void