背景
业务需要一个支持批量操作的下拉框树状数据选择功能,批量操作是可向下级联
所有子节点,批量能力可动态关闭/开启
,需要拿到的数据是所有
选中节点的信息。
看了下antd3中TreeSelect树选择组件提供的showCheckedStrategy属性,不太能满足需求,但树状数据选择能力可用,故基于组件基础能力进行能力扩展,实现TreeSelectCascade组件。
TreeSelect.SHOW_ALL
: 显示所有选中节点(包括父节点)。TreeSelect.SHOW_PARENT
: 只显示父节点(当父节点下所有子节点都选中时)。TreeSelect.SHOW_CHILD
只显示子节点。
思考过程
重点解决2方面问题
- 向下级联的逻辑实现。
- 放开TreeSelect最基础选择能力,
treeCheckStrictly
置为true
。 - 结合onChange暴露的trggerNode动态计算子孙节点,实现级联填充。
- 放开TreeSelect最基础选择能力,
- 级联开关的侵入
- 难点:antd3没有antd4提供的
dropdownRender
属性,导致下拉框内容不可定制。 - 取巧:默认一个根节点,作为级联开关,利用title可为ReactNode属性,放置Checkbox进行实现。
- 掩饰:利用css与节点属性,将根节点控制能力隐藏,暴露给用户的是title中的checkbox。
- 难点:antd3没有antd4提供的
实现
在线示例:体验地址
组件实现,支持表单能力value,onChange
js
import React, { Fragment } from 'react';
import { Form, TreeSelect, Checkbox } from 'antd';
import './index.css';
const { TreeNode } = TreeSelect;
class TreeSelectCascade extends React.Component {
state = {
isCascade: false,
};
changeCascade = (e) => {
this.setState({
isCascade: e.target.checked,
});
};
getAllSubNodesMap = (treeData) => {
const nodeIdItemMap = {};
const traverse = (treeData) => {
treeData.forEach((node) => {
const _node = node.props;
nodeIdItemMap[_node.value] = { value: _node.value, title: _node.title };
if (_node.children && _node.children.length > 0) {
traverse(_node.children);
}
});
};
traverse(treeData);
return nodeIdItemMap;
};
handleChange = (data, label, extra) => {
const { isCascade } = this.state;
const { checked, triggerNode } = extra;
let returnData = [];
if (isCascade) {
const dataMap = data.reduce((total, item) => {
total[item.value] = item;
return total;
}, {});
const relatedNodesMap = this.getAllSubNodesMap([triggerNode]);
if (checked) {
Object.assign(dataMap, relatedNodesMap);
} else {
Object.keys(relatedNodesMap).forEach((value) => {
delete dataMap[value];
});
}
returnData = Object.values(dataMap);
} else {
returnData = data;
}
const { onChange } = this.props;
if (onChange && typeof onChange === 'function') {
onChange(returnData);
}
};
render() {
const { isCascade } = this.state;
const { value = [], treeData, ...extraProps } = this.props;
const loop = (data) => {
return data.map((item) => {
if (item.children && item.children.length > 0) {
return (
<TreeNode value={item.value} title={item.title} key={item.value}>
{loop(item.children)}
</TreeNode>
);
} else {
return (
<TreeNode value={item.value} title={item.title} key={item.value} />
);
}
});
};
return (
<Fragment>
<TreeSelect
dropdownClassName="tree-select-cascade"
filterTreeNode={(inputValue, TreeNode) =>
TreeNode.props.title.indexOf(inputValue) != -1
}
showSearch={true}
treeCheckable={true}
treeCheckStrictly={true}
showCheckedStrategy={TreeSelect.SHOW_ALL}
getPopupContainer={(triggerNode) => triggerNode.parentElement}
style={{ width: '100%' }}
dropdownStyle={{
maxHeight: 300,
overflow: 'auto',
}}
{...extraProps} // 以上可被覆写,以下不可
value={value}
onChange={this.handleChange}
>
<TreeNode
value="-1"
disabled={true}
checkable={false}
title={
<Checkbox checked={isCascade} onChange={this.changeCascade}>
级联勾选子节点
</Checkbox>
}
key="-1"
className="cascade-trigger"
/>
{loop(treeData)}
</TreeSelect>
</Fragment>
);
}
}
export default Form.create()(TreeSelectCascade);
样式文件
less
.tree-select-cascade {
li.ant-select-tree-treenode-disabled > span:not(.ant-select-tree-switcher),
li.ant-select-tree-treenode-disabled > .ant-select-tree-node-content-wrapper,
li.ant-select-tree-treenode-disabled
> .ant-select-tree-node-content-wrapper
span {
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
}
.cascade-trigger {
color: green;
position: absolute;
top: -4px;
right: 4px;
}
}
组件调用
js
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import './index.css';
import { TreeSelect } from 'antd';
import TreeSelectCascade from './TreeSelectCascade';
const { SHOW_PARENT } = TreeSelect;
const treeData = [
{
title: 'Node1',
value: '0-0',
key: '0-0',
children: [
{
title: 'Child Node1',
value: '0-0-0',
key: '0-0-0',
},
],
},
{
title: 'Node2',
value: '0-1',
key: '0-1',
children: [
{
title: 'Child Node3',
value: '0-1-0',
key: '0-1-0',
},
{
title: 'Child Node4',
value: '0-1-1',
key: '0-1-1',
},
{
title: 'Child Node5',
value: '0-1-2',
key: '0-1-2',
},
],
},
];
class Demo extends React.Component {
state = {
value: [{ value: '0-0-0', title: 'Child Node1' }],
};
onChange = (value) => {
console.log('onChange ', value);
this.setState({ value });
};
render() {
const tProps = {
treeData,
value: this.state.value,
onChange: this.onChange,
searchPlaceholder: 'Please select',
style: {
width: '100%',
},
};
return <TreeSelectCascade {...tProps} />;
}
}
ReactDOM.render(<Demo />, document.getElementById('container'));