基于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'));
相关推荐
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9152 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼3 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風7 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#