Tree 树形控件
如果项目中使用的是 0.x 版本的基础组件(@icedesign/base, @ali/ice, @alife/next),请在左侧导航顶部切换组件版本。
安装方法
- 在命令行中执行以下命令
npm install @alifd/next@latest -S
开发指南
何时使用
文件夹、组织架构、生物分类、国家地区等等,世间万物的大多数结构都是树形结构。使用树控件
可以完整展现其中的层级关系,并具有展开收起选择等交互功能。
API
Tree
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
children | 树节点 | ReactNode | - |
dataSource | 数据源,该属性优先级高于 children | Array | - |
showLine | 是否显示树的线 | Boolean | false |
selectable | 是否支持选中节点 | Boolean | true |
selectedKeys | (用于受控)当前选中节点 key 的数组 | Array<String> | - |
defaultSelectedKeys | (用于非受控)默认选中节点 key 的数组 | Array<String> | [] |
onSelect | 选中或取消选中节点时触发的回调函数签名:Function(selectedKeys: Array, extra: Object) => void参数:selectedKeys: {Array} 选中节点key的数组extra: {Object} 额外参数extra.selectedNodes: {Array} 选中节点的数组extra.node: {Object} 当前操作的节点extra.selected: {Boolean} 当前操作是否是选中 | Function | func.noop |
multiple | 是否支持多选 | Boolean | false |
checkable | 是否支持勾选节点的复选框 | Boolean | false |
checkedKeys | (用于受控)当前勾选复选框节点 key 的数组或 {checked: Array, indeterminate: Array} 的对象 | Array<String>/Object | - |
defaultCheckedKeys | (用于非受控)默认勾选复选框节点 key 的数组 | Array<String> | [] |
checkStrictly | 勾选节点复选框是否完全受控(父子节点选中状态不再关联) | Boolean | false |
checkedStrategy | 定义选中时回填的方式可选值:'all'(返回所有选中的节点)'parent'(父子节点都选中时只返回父节点)'child'(父子节点都选中时只返回子节点) | Enum | 'all' |
onCheck | 勾选或取消勾选复选框时触发的回调函数签名:Function(checkedKeys: Array, extra: Object) => void参数:checkedKeys: {Array} 勾选复选框节点key的数组extra: {Object} 额外参数extra.checkedNodes: {Array} 勾选复选框节点的数组extra.checkedNodesPositions: {Array} 包含有勾选复选框节点和其位置的对象的数组extra.indeterminateKeys: {Array} 半选复选框节点 key 的数组extra.node: {Object} 当前操作的节点extra.checked: {Boolean} 当前操作是否是勾选 | Function | func.noop |
expandedKeys | (用于受控)当前展开的节点 key 的数组 | Array<String> | - |
defaultExpandedKeys | (用于非受控)默认展开的节点 key 的数组 | Array<String> | [] |
defaultExpandAll | 是否默认展开所有节点 | Boolean | false |
autoExpandParent | 是否自动展开父节点 | Boolean | true |
onExpand | 展开或收起节点时触发的回调函数签名:Function(expandedKeys: Array, extra: Object) => void参数:expandedKeys: {Array} 展开的节点key的数组extra: {Object} 额外参数extra.node: {Object} 当前操作的节点extra.expanded: {Boolean} 当前操作是否是展开 | Function | func.noop |
editable | 是否支持编辑节点内容 | Boolean | false |
onEditFinish | 编辑节点内容完成时触发的回调函数签名:Function(key: String, label: String, node: Object) => void参数:key: {String} 编辑节点的 keylabel: {String} 编辑节点完成时节点的文本node: {Object} 当前编辑的节点 | Function | func.noop |
draggable | 是否支持拖拽节点 | Boolean | false |
onDragStart | 开始拖拽节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 拖拽的节点 | Function | func.noop |
onDragEnter | 拖拽节点进入目标节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 目标节点info.expandedKeys: {Array} 当前展开的节点key的数组 | Function | func.noop |
onDragOver | 拖拽节点在目标节点上移动的时候触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 目标节点 | Function | func.noop |
onDragLeave | 拖拽节点离开目标节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 目标节点 | Function | func.noop |
onDragEnd | 拖拽节点拖拽结束时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 目标节点 | Function | func.noop |
onDrop | 拖拽节点放入目标节点内或前后触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {Object} 目标节点info.dragNode: {Object} 拖拽的节点info.dragNodesKeys: {Array} 拖拽的节点和其子节点 key 的数组info.dropPosition: {Number} 放置位置,-1代表当前节点前,0代表当前节点里,1代表当前节点后 | Function | func.noop |
canDrop | 节点是否可被作为拖拽的目标节点签名:Function(info: Object) => Boolean参数:info: {Object} 拖拽信息info.node: {Object} 目标节点info.dragNode: {Object} 拖拽的节点info.dragNodesKeys: {Array} 拖拽的节点和其子节点 key 的数组info.dropPosition: {Number} 放置位置,-1代表当前节点前,0代表当前节点里,1代表当前节点后返回值:{Boolean} 是否可以被当作目标节点 | Function | () => true |
loadData | 异步加载数据的函数签名:Function(node: Object) => void参数:node: {Object} 被点击展开的节点 | Function | - |
filterTreeNode | 按需筛选高亮节点签名:Function(node: Object) => Boolean参数:node: {Object} 待筛选的节点返回值:{Boolean} 是否被筛选中 | Function | - |
onRightClick | 右键点击节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 信息对象info.event: {Object} 事件对象info.node: {Object} 点击的节点 | Function | func.noop |
isLabelBlock | 设置节点是否占满剩余空间,一般用于统一在各节点右侧添加元素(借助 flex 实现,暂时只支持 ie10+) | Boolean | false |
isNodeBlock | 设置节点是否占满一行 | Boolean/Object | false |
animation | 是否开启展开收起动画 | Boolean | true |
focusedKey | 当前获得焦点的子菜单或菜单项 key 值 | String | - |
Tree.Node
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
children | 树节点 | ReactNode | - |
label | 节点文本内容 | ReactNode | '—-' |
selectable | 单独设置是否支持选中,覆盖 Tree 的 selectable | Boolean | - |
checkable | 单独设置是否出现复选框,覆盖 Tree 的 checkable | Boolean | - |
editable | 单独设置是否支持编辑,覆盖 Tree 的 editable | Boolean | - |
draggable | 单独设置是否支持拖拽,覆盖 Tree 的 draggable | Boolean | - |
disabled | 是否禁止节点响应 | Boolean | false |
checkboxDisabled | 是否禁止勾选节点复选框 | Boolean | false |
isLeaf | 是否是叶子节点,设置loadData时生效 | Boolean | false |
key
值:<TreeNode key="102894" label="女装" />
,一般为数据的 id 值,但必需保证其全局唯一性,key
的默认值为 Tree 内部计算出的位置字符串。<!— api-extra-end —>
代码示例
最简单的用法,展示可展开,可选中,可勾选,可编辑,可右键,禁用,禁用勾选,默认展开,默认选中,默认勾选等功能。
查看源码在线预览
import { Tree } from '@alifd/next';
const TreeNode = Tree.Node;
class Demo extends React.Component {
onSelect(keys, info) {
console.log('onSelect', keys, info);
}
onCheck(keys, info) {
console.log('onCheck', keys, info);
}
onEditFinish(key, label, node) {
console.log('onEditFinish', key, label, node);
}
onRightClick(info) {
console.log('onRightClick', info);
}
render() {
return (
<Tree checkable editable
defaultExpandedKeys={['2']}
defaultCheckedKeys={['2', '4', '5']}
onSelect={this.onSelect}
onCheck={this.onCheck}
onEditFinish={this.onEditFinish}
onRightClick={this.onRightClick}>
<TreeNode key="1" label="Component">
<TreeNode key="2" label="Form" selectable={false}>
<TreeNode key="4" label="Input" />
<TreeNode key="5" label="Select" disabled />
</TreeNode>
<TreeNode key="3" label="Display">
<TreeNode key="6" label="Table" />
</TreeNode>
</TreeNode>
</Tree>
);
}
}
ReactDOM.render(<Demo />, mountNode);
使用 dataSource 生成树结构,除设置 key, label, children 属性外,还可传入 TreeNode 的其他属性,包括 selectable,disabled,checkable, checkboxDisabled, isLeaf 等。
查看源码在线预览
import { Tree } from '@alifd/next';
const data = [{
label: 'Component',
key: '1',
children: [{
label: 'Form',
key: '2',
selectable: false,
children: [{
label: 'Input',
key: '4'
}, {
label: 'Select',
key: '5',
disabled: true
}]
}, {
label: 'Display',
key: '3',
children: [{
label: 'Table',
key: '6'
}]
}]
}];
class Demo extends React.Component {
onSelect(keys, info) {
console.log('onSelect', keys, info);
}
onCheck(keys, info) {
console.log('onCheck', keys, info);
}
onEditFinish(key, label, node) {
console.log('onEditFinish', key, label, node);
}
onRightClick(info) {
console.log('onRightClick', info);
}
render() {
return (
<Tree checkable editable
defaultExpandedKeys={['2']}
defaultCheckedKeys={['2', '4', '5']}
onSelect={this.onSelect}
onCheck={this.onCheck}
onEditFinish={this.onEditFinish}
onRightClick={this.onRightClick}
dataSource={data} />
);
}
}
ReactDOM.render(<Demo />, mountNode);
展示Tree组件带线的样式外观。
查看源码在线预览
import { Tree } from '@alifd/next';
const TreeNode = Tree.Node;
ReactDOM.render(
<Tree defaultExpandAll showLine>
<TreeNode label="Trunk">
<TreeNode label="Branch">
<TreeNode label="Branch">
<TreeNode label="Leaf" />
</TreeNode>
<TreeNode label="Leaf" />
</TreeNode>
<TreeNode label="Branch">
<TreeNode label="Leaf" />
<TreeNode label="Leaf" />
</TreeNode>
</TreeNode>
</Tree>, mountNode);
可以通过设置 isNodeBlock 为 true,来让树节点占满一行,isNodeBlock 也可传入一个对象,支持设置 defaultPaddingLeft(默认的左内边距)和 indent (缩进距离),另外注意 showLine 在开启 isNodeBlock 时失效。
查看源码在线预览
import { Tree } from '@alifd/next';
const TreeNode = Tree.Node;
ReactDOM.render(
<Tree defaultExpandAll isNodeBlock={{ defaultPaddingLeft: 50 }} defaultSelectedKeys={['1']} style={{ width: '300px' }}>
<TreeNode label="Component" key="0">
<TreeNode label="Form" key="1" disabled>
<TreeNode label="Select" key="2">
<TreeNode label="TreeSelect" key="3" />
</TreeNode>
<TreeNode label="Input" key="4" />
</TreeNode>
<TreeNode label="Display" key="5">
<TreeNode label="Card" key="6" />
<TreeNode label="Table" key="7" />
</TreeNode>
</TreeNode>
</Tree>, mountNode);
展示单选与多选的用法。
查看源码在线预览
import { Checkbox, Tree } from '@alifd/next';
const data = [{
key: '0-0',
label: '0-0',
children: [{
key: '0-0-0',
label: '0-0-0',
children: [{
key: '0-0-0-0',
label: '0-0-0-0',
children: [{
key: '0-0-0-0-0',
label: '0-0-0-0-0'
}]
}, {
key: '0-0-0-1',
label: '0-0-0-1'
}]
}, {
key: '0-0-1',
label: '0-0-1',
children: [{
key: '0-0-1-0',
label: '0-0-1-0'
}, {
key: '0-0-1-1',
label: '0-0-1-1'
}]
}]
}];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedKeys: [],
multiple: false
};
this.handleSelect = this.handleSelect.bind(this);
this.handleCheck = this.handleCheck.bind(this);
}
handleSelect(keys, info) {
console.log(keys, info);
this.setState({
selectedKeys: keys
});
}
handleCheck() {
this.setState({
multiple: !this.state.multiple,
selectedKeys: []
});
}
render() {
const { multiple, selectedKeys } = this.state;
return (
<div className="control-select-demo">
<label className="multiple-check">
<Checkbox value={multiple} onChange={this.handleCheck} />
<span className="multiple-text">Enable multiple</span>
</label>
<Tree defaultExpandAll multiple={multiple} selectedKeys={selectedKeys} onSelect={this.handleSelect} dataSource={data} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.control-select-demo .multiple-check {
display: block;
margin-bottom: 10px;
}
.control-select-demo .multiple-text {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
color: #666;
font-size: 14px;
}
展示父子节点选中是否关联的用法。
查看源码在线预览
import { Checkbox, Tree } from '@alifd/next';
const data = [{
key: '0-0',
label: '0-0',
children: [{
key: '0-0-0',
label: '0-0-0',
children: [{
key: '0-0-0-0',
label: '0-0-0-0',
children: [{
key: '0-0-0-0-0',
label: '0-0-0-0-0'
}]
}, {
key: '0-0-0-1',
label: '0-0-0-1'
}]
}, {
key: '0-0-1',
label: '0-0-1',
children: [{
key: '0-0-1-0',
label: '0-0-1-0'
}, {
key: '0-0-1-1',
label: '0-0-1-1'
}]
}]
}];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
checkedKeys: [],
checkStrictly: false
};
this.handleCheck = this.handleCheck.bind(this);
this.handleCheckStrictly = this.handleCheckStrictly.bind(this);
}
handleCheck(keys, info) {
console.log(keys, info);
this.setState({
checkedKeys: keys
});
}
handleCheckStrictly() {
this.setState({
checkStrictly: !this.state.checkStrictly,
checkedKeys: []
});
}
render() {
const { checkedKeys, checkStrictly } = this.state;
return (
<div className="control-check-demo">
<label className="strictly-check">
<Checkbox value={checkStrictly} onChange={this.handleCheckStrictly} />
<span className="strictly-text">Enable checkStrictly</span>
</label>
<Tree defaultExpandAll checkable checkStrictly={checkStrictly} checkedKeys={checkedKeys} onCheck={this.handleCheck} dataSource={data} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.control-check-demo .strictly-check {
display: block;
margin-bottom: 10px;
}
.control-check-demo .strictly-text {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
color: #666;
font-size: 14px;
}
展示可搜索的树。
查看源码在线预览
import { Search, Tree } from '@alifd/next';
const data = [{
label: 'Component',
key: '1',
children: [{
label: 'Form',
key: '2',
children: [{
label: 'Input',
key: '4'
}, {
label: 'Select',
key: '5'
}]
}, {
label: 'Display',
key: '3',
children: [{
label: 'Table',
key: '6'
}]
}]
}];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
expandedKeys: ['2'],
autoExpandParent: true
};
this.matchedKeys = [];
this.handleSearch = this.handleSearch.bind(this);
this.handleExpand = this.handleExpand.bind(this);
}
handleSearch(value) {
value = value.trim();
if (!value) {
this.matchedKeys = null;
return;
}
const matchedKeys = [];
const loop = data => data.forEach(item => {
if (item.label.indexOf(value) > -1) {
matchedKeys.push(item.key);
}
if (item.children && item.children.length) {
loop(item.children);
}
});
loop(data);
this.setState({
expandedKeys: [...matchedKeys],
autoExpandParent: true
});
this.matchedKeys = matchedKeys;
}
handleExpand(keys) {
this.setState({
expandedKeys: keys,
autoExpandParent: false
});
}
render() {
const { expandedKeys, autoExpandParent } = this.state;
const filterTreeNode = node => {
return this.matchedKeys && this.matchedKeys.indexOf(node.props.eventKey) > -1;
};
return (
<div>
<Search shape="simple" size="medium" style={{ width: '200px', marginBottom: '10px' }} onChange={this.handleSearch} />
<Tree expandedKeys={expandedKeys} autoExpandParent={autoExpandParent} filterTreeNode={filterTreeNode} onExpand={this.handleExpand} dataSource={data} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
点击展开节点,动态加载数据。
查看源码在线预览
import { Tree } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [
{ label: 'Expand to load', key: '0' },
{ label: 'Expand to load', key: '1' },
{ label: 'Leaf', key: '2', isLeaf: true }
]
};
this.onLoadData = this.onLoadData.bind(this);
}
onLoadData(node) {
return new Promise(resolve => {
if (node.props.children) {
return resolve();
}
const { eventKey, pos } = node.props;
const item = this.getItemByPos(pos);
setTimeout(() => {
item.children = [
{ label: 'Child Tree', key: `${eventKey}-0` },
{ label: 'Child Tree', key: `${eventKey}-1` }
];
this.setState({
data: [...this.state.data]
});
resolve();
}, 1000);
});
}
getItemByPos(pos) {
return pos.split('-').slice(1).reduce((ret, num) => ret.children[num], { children: this.state.data });
}
render() {
return (
<Tree dataSource={this.state.data} loadData={this.onLoadData} />
);
}
}
ReactDOM.render(<Demo />, mountNode);
将节点拖拽到其他节点内部或前后。
查看源码在线预览
import { Tree } from '@alifd/next';
const TreeNode = Tree.Node;
const x = 3;
const y = 2;
const z = 1;
const gData = [];
const generateData = (_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 });
if (i < y) {
children.push(key);
}
}
if (_level < 0) {
return tns;
}
const level = _level - 1;
children.forEach((key, index) => {
tns[index].children = [];
return generateData(level, key, tns[index].children);
});
};
generateData(z);
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
gData
};
}
onDrop(info) {
if (!info.dragNode) {
return;
}
const dragKey = info.dragNode.props.eventKey;
const dropKey = info.node.props.eventKey;
const dropPosition = info.dropPosition;
const loop = (data, key, callback) => {
data.forEach((item, index, arr) => {
if (item.key === key) {
return callback(item, index, arr);
}
if (item.children) {
return loop(item.children, key, callback);
}
});
};
const data = [...this.state.gData];
let dragObj;
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
dragObj = item;
});
if (info.dropPosition === 0) {
loop(data, dropKey, (item) => {
item.children = item.children || [];
item.children.push(dragObj);
});
} else {
let ar;
let i;
loop(data, dropKey, (item, index, arr) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
} else {
ar.splice(i + 1, 0, dragObj);
}
}
this.setState({
gData: data
});
}
render() {
const loop = data => data.map(item => {
if (item.children) {
return <TreeNode key={item.key} label={item.key}>{loop(item.children)}</TreeNode>;
}
return <TreeNode key={item.key} label={item.key} />;
});
return (
<Tree draggable selectable={false} isLabelBlock defaultExpandedKeys={['0-0', '0-0-0', '0-0-0-0']} onDrop={this.onDrop.bind(this)}>
{loop(this.state.gData)}
</Tree>
);
}
}
ReactDOM.render(<Demo />, mountNode);