Tree 树形控件
如果项目中使用的是 1.x 版本的基础组件(@alifd/next),请在左侧导航顶部切换组件版本。
安装方法
- 在命令行中执行以下命令
npm install @icedesign/base@latest -S
何时使用
文件夹、组织架构、生物分类、国家地区等等,世间万物的大多数结构都是树形结构。使用树控件
可以完整展现其中的层级关系,并具有展开收起选择等交互功能。
API
树形控件
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
prefix | 样式类名的品牌前缀 | String | 'next-' |
className | 自定义类名 | String | - |
style | 自定义内联样式 | Object | - |
children | 树节点 | ReactNode | - |
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: {ReactElement} 当前操作的节点extra.selected: {Boolean} 当前操作是否是选中extra.event: {String} 当前操作的类型,值为'select' | Function | () => {} |
multiple | 是否支持多选 | Boolean | false |
checkable | 是否支持勾选节点的复选框 | Boolean | false |
checkedKeys | (用于受控)当前勾选复选框节点key的数组或{checked: Array, halfChecked: Array} 的对象 | Array<String>/Object | - |
defaultCheckedKeys | (用于非受控)默认勾选复选框节点key的数组 | Array<String> | [] |
checkStrictly | 勾选节点复选框是否完全受控(父子节点选中状态不再关联) | Boolean | false |
enableCheckedCache | 是否启用勾选节点复选框的缓存来提高性能,如果dataSource需要被动态更新,请将其设置为false | Boolean | true |
onCheck | 勾选或取消勾选复选框时触发的回调函数签名:Function(checkedKeys: Array, extra: Object) => void参数:checkedKeys: {Array} 勾选复选框节点key的数组extra: {Object} 额外参数extra.checkedNodes: {Array} 勾选复选框节点的数组extra.checkedNodesPositions: {Array} 包含有勾选复选框节点和其位置的对象的数组extra.halfCheckedKeys: {Array} 半选复选框节点key的数组extra.node: {ReactElement} 当前操作的节点extra.checked: {Boolean} 当前操作是否是勾选extra.event: {String} 当前操作的类型,值为'check' | Function | () => {} |
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: {ReactElement} 当前操作的节点extra.expanded: {Boolean} 当前操作是否是展开 | Function | () => {} |
editable | 是否支持编辑节点内容 | Boolean | false |
onEditFinish | 编辑节点内容完成时触发的回调函数签名:Function(key: String, label: String) => void参数:key: {String} 编辑节点的keylabel: {String} 编辑节点完成时节点的文本 | Function | () => {} |
draggable | 是否支持拖拽节点 | Boolean | false |
onDragStart | 开始拖拽节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {ReactElement} 拖拽的节点 | Function | () => {} |
onDragEnter | 拖拽节点进入目标节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {ReactElement} 目标节点info.expandedKeys: {Array} 当前展开的节点key的数组 | Function | () => {} |
onDragOver | 拖拽节点在目标节点上移动的时候触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {ReactElement} 目标节点 | Function | () => {} |
onDragLeave | 拖拽节点离开目标节点时触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {ReactElement} 目标节点 | Function | () => {} |
onDrop | 拖拽节点放入目标节点内或前后触发的回调函数签名:Function(info: Object) => void参数:info: {Object} 拖拽信息info.event: {Object} 事件对象info.node: {ReactElement} 目标节点info.dragNode: {ReactElement} 拖拽的节点info.dragNodesKeys: {Array} 拖拽的节点和其子节点key的数组info.dropPosition: {Number} 拖拽的节点在拖拽后被放置在当前层级的位置info.dropToGap: {Boolean} 是否被放置在目标节点的前后(没有被放置在目标节点内部) | Function | () => {} |
canDrop | 节点是否可被作为拖拽的目标节点签名:Function(info: Object) => Boolean参数:info: {Object} 拖拽信息info.node: {ReactElement} 目标节点info.dragNode: {ReactElement} 拖拽的节点info.dragNodesKeys: {Array} 拖拽的节点和其子节点key的数组info.dropPosition: {Number} 拖拽的节点在拖拽后被放置在当前层级的位置info.dropToGap: {Boolean} 是否被放置在目标节点的前后(没有被放置在目标节点内部)返回值:{Boolean} 是否可以被当作目标节点 | Function | () => true |
loadData | 异步加载数据的函数签名:Function(node: ReactElement) => void参数:node: {ReactElement} 被点击展开的节点 | Function | - |
filterTreeNode | 按需筛选高亮节点签名:Function(node: ReactElement) => Boolean参数:node: {ReactElement} 待筛选的节点返回值:{Boolean} 是否被筛选中 | Function | - |
onRightClick | 右键点击节点时触发的回调函数签名:Function(event: Object, node: ReactElement) => void参数:event: {Object} 事件对象node: {ReactElement} 点击的节点 | Function | - |
isLabelBlock | 设置节点是否占满剩余空间,一般用于统一在各节点右侧添加元素(借助flex实现,暂时只支持ie10+) | Boolean | false |
animation | 是否开启展开收起动画 | Boolean | true |
Tree.Node
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
prefix | 样式类名的品牌前缀 | String | - |
className | 自定义类名 | String | - |
style | 自定义内联样式 | Object | - |
children | 树节点 | ReactNode | - |
label | 节点文本内容 | ReactNode | '—-' |
selectable | 单独设置是否支持选中,覆盖Tree的selectable | Boolean | - |
editable | 单独设置是否支持编辑,覆盖Tree的editable | Boolean | - |
draggable | 单独设置是否支持拖拽,覆盖Tree的draggable | Boolean | - |
disabled | 是否禁止节点响应 | Boolean | false |
disableCheckbox | 是否禁止勾选节点复选框 | Boolean | false |
isLeaf | 是否是叶子节点,设置loadData时生效 | Boolean | false |
代码示例
最简单的用法,展示可展开,可选中,可勾选,可编辑,可右键,禁用,禁用勾选,默认展开,默认选中,默认勾选等功能。
查看源码在线预览
import { Tree } from "@icedesign/base";
const { Node: TreeNode } = Tree;
class Demo extends React.Component {
onSelect(keys, info) {
console.log("onSelect", keys, info);
}
onCheck(keys, info) {
console.log("onCheck", keys, info);
}
onEditFinish(info) {
console.log("onEditFinish", info);
}
onRightClick(info) {
console.log("onRightClick", info);
}
render() {
return (
<Tree
multiple
checkable
editable
defaultExpandedKeys={["0-0-0", "0-0-1"]}
defaultCheckedKeys={["0-0-0", "0-0-1"]}
onSelect={this.onSelect}
onCheck={this.onCheck}
onEditFinish={this.onEditFinish}
onRightClick={this.onRightClick}
>
<TreeNode label="parent 1" key="0-0">
<TreeNode label="parent 1-0" key="0-0-0" disabled>
<TreeNode label="leaf" key="0-0-0-0" disableCheckbox />
<TreeNode label="leaf" key="0-0-0-1" />
</TreeNode>
<TreeNode label="parent 1-1" key="0-0-1">
<TreeNode
label={<span style={{ color: "#08c" }}>abc</span>}
key="0-0-1-0"
/>
</TreeNode>
</TreeNode>
</Tree>
);
}
}
ReactDOM.render(<Demo />, mountNode);
展示父子节点选中是否关联的用法。
查看源码在线预览
import { Tree, Checkbox } from "@icedesign/base";
const { Node: TreeNode } = Tree;
const data = [
{
id: "0-0",
children: [
{
id: "0-0-0",
children: [
{
id: "0-0-0-0",
children: [
{
id: "0-0-0-0-0"
}
]
},
{
id: "0-0-0-1"
}
]
},
{
id: "0-0-1",
children: [
{
id: "0-0-1-0"
},
{
id: "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 loop = data =>
data.map(item => {
return (
<TreeNode label={item.id} key={item.id}>
{item.children && item.children.length ? loop(item.children) : null}
</TreeNode>
);
});
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">
开启严格受控,父子节点选中不再关联
</span>
</label>
<Tree
defaultExpandAll
checkable
checkStrictly={checkStrictly}
checkedKeys={checkedKeys}
onCheck={this.handleCheck}
>
{loop(data)}
</Tree>
</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 { Tree, Checkbox } from "@icedesign/base";
const { Node: TreeNode } = Tree;
const data = [
{
id: "0-0",
children: [
{
id: "0-0-0",
children: [
{
id: "0-0-0-0",
children: [
{
id: "0-0-0-0-0"
}
]
},
{
id: "0-0-0-1"
}
]
},
{
id: "0-0-1",
children: [
{
id: "0-0-1-0"
},
{
id: "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 loop = data =>
data.map(item => {
return (
<TreeNode label={item.id} key={item.id}>
{item.children && item.children.length ? loop(item.children) : null}
</TreeNode>
);
});
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">开启多选</span>
</label>
<Tree
defaultExpandAll
multiple={multiple}
selectedKeys={selectedKeys}
onSelect={this.handleSelect}
>
{loop(data)}
</Tree>
</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 { Tree } from "@icedesign/base";
const { Node: TreeNode } = Tree;
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,
expandedKeys: ["0-0", "0-0-0", "0-0-0-0"]
};
}
// eslint-disable-next-line
onDragEnter(info) {
// expandedKeys 需要受控时设置
// this.setState({
// expandedKeys: info.expandedKeys,
// });
}
onDrop(info) {
const dragKey = info.dragNode.props.eventKey;
const dropKey = info.node.props.eventKey;
const dropPos = info.node.props.pos.split("-");
// 通过info.node.props.pos计算得到的dropPos为-1则为拖动到节点之上,1则为拖动到节点之下
const dropPosition =
info.dropPosition - Number(dropPos[dropPos.length - 1]);
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;
});
// dropToGap为true 则拖动到节点内部
if (info.dropToGap) {
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);
}
} else {
loop(data, dropKey, item => {
item.children = item.children || [];
// where to insert 示例添加到尾部,可以是随意位置
item.children.push(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
defaultExpandedKeys={this.state.expandedKeys}
draggable
selectable={false}
onDragEnter={this.onDragEnter.bind(this)}
onDrop={this.onDrop.bind(this)}
>
{loop(this.state.gData)}
</Tree>
);
}
}
ReactDOM.render(<Demo />, mountNode);
点击展开节点,动态加载数据。
查看源码在线预览
import { Tree } from "@icedesign/base";
const { Node: TreeNode } = Tree;
function generateTreeNodes(treeNode) {
const arr = [];
const key = treeNode.props.eventKey;
for (let i = 0; i < 3; i++) {
arr.push({ name: `leaf ${key}-${i}`, key: `${key}-${i}` });
}
return arr;
}
function setLeaf(treeData, curKey, level) {
const loopLeaf = (data, lev) => {
const l = lev - 1;
data.forEach(item => {
if (
item.key.length > curKey.length
? item.key.indexOf(curKey) !== 0
: curKey.indexOf(item.key) !== 0
) {
return;
}
if (item.children) {
loopLeaf(item.children, l);
} else if (l < 1) {
item.isLeaf = true;
}
});
};
loopLeaf(treeData, level + 1);
}
function getNewTreeData(treeData, curKey, child, level) {
const loop = data => {
if (level < 1 || curKey.length - 3 > level * 2) {
return;
}
data.forEach(item => {
if (curKey.indexOf(item.key) === 0) {
if (item.children) {
loop(item.children);
} else {
item.children = child;
}
}
});
};
loop(treeData);
setLeaf(treeData, curKey, level);
}
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
treeData: []
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
treeData: [
{ name: "pNode 01", key: "0-0" },
{ name: "pNode 02", key: "0-1" },
{ name: "pNode 03", key: "0-2", isLeaf: true }
]
});
}, 100);
}
onSelect(info) {
console.log("selected", info);
}
onLoadData(treeNode) {
return new Promise(resolve => {
setTimeout(() => {
const treeData = [...this.state.treeData];
getNewTreeData(
treeData,
treeNode.props.eventKey,
generateTreeNodes(treeNode),
2
);
this.setState({ treeData });
resolve();
}, 500);
});
}
render() {
const loop = data =>
data.map(item => {
if (item.children) {
return (
<TreeNode label={item.name} key={item.key}>
{loop(item.children)}
</TreeNode>
);
}
return (
<TreeNode
label={item.name}
key={item.key}
isLeaf={item.isLeaf}
disabled={item.key === "0-0-0"}
/>
);
});
const treeNodes = loop(this.state.treeData);
return (
<Tree
onSelect={this.onSelect.bind(this)}
loadData={this.onLoadData.bind(this)}
>
{treeNodes}
</Tree>
);
}
}
ReactDOM.render(<Demo />, mountNode);
展示Tree组件带线的样式外观。
查看源码在线预览
import { Tree } from "@icedesign/base";
const { Node: TreeNode } = Tree;
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
);
展示可搜索的树。
查看源码在线预览
import { Tree, Search } from "@icedesign/base";
const { Node: TreeNode } = Tree;
const data = [
{
id: "0-0",
children: [
{
id: "0-0-0",
children: [
{
id: "0-0-0-0",
children: [
{
id: "0-0-0-0-0"
}
]
},
{
id: "0-0-0-1"
}
]
},
{
id: "0-0-1",
children: [
{
id: "0-0-1-0"
},
{
id: "0-0-1-1"
}
]
}
]
}
];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
expandedKeys: ["0-0"],
autoExpandParent: true
};
this.matchedKeys = [];
this.handleSearch = this.handleSearch.bind(this);
this.handleExpand = this.handleExpand.bind(this);
}
handleSearch(result) {
const value = result.key;
const matchedKeys = [];
const loop = data =>
data.forEach(item => {
if (item.id.indexOf(value.trim()) > -1) {
matchedKeys.push(item.id);
}
if (item.children && item.children.length) {
loop(item.children);
}
});
loop(data);
this.setState({
value: result.key,
expandedKeys: matchedKeys,
autoExpandParent: true
});
this.matchedKeys = matchedKeys;
}
handleExpand(keys) {
this.setState({
expandedKeys: keys,
autoExpandParent: false
});
}
render() {
const loop = data =>
data.map(item => {
return (
<TreeNode label={item.id} key={item.id}>
{item.children && loop(item.children)}
</TreeNode>
);
});
const { value, expandedKeys, autoExpandParent } = this.state;
const filterTreeNode = node =>
value && this.matchedKeys.indexOf(node.props.eventKey) > -1;
return (
<div>
<Search
type="normal"
size="medium"
searchText=""
value={value}
onSearch={this.handleSearch}
/>
<Tree
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
filterTreeNode={filterTreeNode}
onExpand={this.handleExpand}
>
{loop(data)}
</Tree>
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);