基于antd3的级联TreeSelect组件实现

背景

业务需要一个支持批量操作的下拉框树状数据选择功能,批量操作是可向下级联所有子节点,批量能力可动态关闭/开启,需要拿到的数据是所有选中节点的信息。

看了下antd3中TreeSelect树选择组件提供的showCheckedStrategy属性,不太能满足需求,但树状数据选择能力可用,故基于组件基础能力进行能力扩展,实现TreeSelectCascade组件。

TreeSelect.SHOW_ALL: 显示所有选中节点(包括父节点)。TreeSelect.SHOW_PARENT: 只显示父节点(当父节点下所有子节点都选中时)。TreeSelect.SHOW_CHILD只显示子节点。

思考过程

重点解决2方面问题

  1. 向下级联的逻辑实现。
    • 放开TreeSelect最基础选择能力,treeCheckStrictly置为true
    • 结合onChange暴露的trggerNode动态计算子孙节点,实现级联填充。
  2. 级联开关的侵入
    • 难点:antd3没有antd4提供的dropdownRender属性,导致下拉框内容不可定制。
    • 取巧:默认一个根节点,作为级联开关,利用title可为ReactNode属性,放置Checkbox进行实现。
    • 掩饰:利用css与节点属性,将根节点控制能力隐藏,暴露给用户的是title中的checkbox。

实现

在线示例:体验地址

组件实现,支持表单能力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'));
相关推荐
dr李四维2 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~23 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ26 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z32 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript