elementUI-cascader-全选功能

4,769 阅读4分钟

先看效果图:

对element-ui的组件二次改造、新增功能有三种方式,请各位务必、务必、务必看这篇文章

我对element-ui(后面用eui简称)项目进行了改造,然后是一个新的eui,姑且称之为aui吧!

这个aui只是在eui的基础上,给cascader新增了很多功能,比如这篇文章要说的cascader全选功能。

aui的github项目地址:地址

各位可以点击链接,直接体验aui。

首先,想了解cascader组件的整体结构的,可以先看这篇文章

我们魔改cascader整个组件的流程如下:

  1. 添加全选按钮,至少得在页面上先看到这个按钮
  2. 添加全选按钮的点击逻辑
  3. 根据cascader-node的集合的check状态去更新全选按钮的状态

第一步:添加全选按钮

cascader-node组件是在cascader-menu组件中渲染的,渲染函数在cascader-menu.vue中,代码如下:

renderNodeList(h) {      
     const { menuId} = this;      
     const { isHoverMenu } = this.panel;     
     const events = { on: {} };      
     if (isHoverMenu) {        
        events.on.expand = this.handleExpand;     
     }     
     // 此处就是要渲染的cascader-node集合   
     const nodes = this.nodes.map((node, index) => {     
         const { hasChildren } = node;        
         return (          
             <cascader-node            
                key={ node.uid }           
                node={ node }            
                node-id={ `${menuId}-${index}` }      
                aria-haspopup={ hasChildren }         
                aria-owns = { hasChildren ? menuId : null }        
                { ...events }></cascader-node>       
          );      
      });      
      // 渲染出cascader-node集合,我们只要这里插入全选组件即可。
      return [        
         ...nodes,        
         isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null     
      ];   
 }

添加全选按钮之后的代码如下:

renderNodeList(h) {
      const { menuId, nodes, checkAll, isIndeterminate, handleCheckAllChange, isCheckAll } = this;
      const { isHoverMenu, config } = this.panel;
      const events = { on: {} };

      if (isHoverMenu) {
        events.on.expand = this.handleExpand;
      }

      const nodeItems = nodes.map((node, index) => {
        const { hasChildren } = node;
        return (
          <cascader-node
            key={ node.uid }
            node={ node }
            node-id={ `${menuId}-${index}` }
            aria-haspopup={ hasChildren }
            aria-owns = { hasChildren ? menuId : null }
            { ...events }></cascader-node>
        );
      });

      return [
        isCheckAll && 
        <el-checkbox 
            class="checkAll" 
            indeterminate={isIndeterminate} 
            value={checkAll} 
            onChange={handleCheckAllChange}
        >
        全选
        </el-checkbox>, 
        [...nodeItems],
        isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
      ];
}

修改完上面是会报错的,还得执行下面的步骤。

首先,我们得给data定义两个变量:checkAllisIndeterminate,这两个变量一起控制全选按钮的展示,后面会介绍。

data() {
    return {
      activeNode: null,
      hoverTimer: null,
      id: generateId(),
      // 添加以下两个
      checkAll: false,
      isIndeterminate: false
    };
},

然后我们定义一个计算属性isCheckAll,来控制全选按钮是否展示

computed: {
    isCheckAll() {
      let config = this.panel.config;
      // 父子关联的话,只有第一个cascader-menu有全选按钮
      // 父子不关联的话,所有的cascader-menu都有全选按钮
      return config.checkAll && (config.checkStrictly || (!config.checkStrictly && this.index === 0));
    }
}

config.checkAll就是给cascader组件传入的props参数。

比如要我要开启全选功能,那么就要传入checkAll: true的参数。

<el-cascader
    :options="options"
    :props="props"
    collapse-tags
    clearable>
</el-cascader>

<script>
    data() {
        return {
            props: { multiple: true, checkAll: true, expandTrigger: 'hover' },
        }
    }
</script>

然后,还是会报错,因为handleCheckAllChange还没写。

第二步:添加全选按钮的点击逻辑

建议先看看el-checkbox这个组件的’全选‘、’没选‘、’没全选‘这三种状态的逻辑,地址

再来看一遍cascader-menu的渲染代码块:

return [        
    isCheckAll && 
    <el-checkbox          
        class="checkAll"          
        indeterminate={isIndeterminate}          
        value={checkAll}          
        onChange={handleCheckAllChange}
    >全选
    </el-checkbox>,       
    ...nodes,       
    isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null   
];

我们给el-checkbox组件添加了两个属性和一个方法:

  • value:true->全选,false->待定。
  • indeterminate: 当value为true,该属性无效;当value为false,该属性为false则没选,为true则没全选。
  • onChange:点击该按钮时触发。

先看onChange的触发函数,请仔细查看代码注释:

handleCheckAllChange() {      
    const { nodes, panel } = this;      
    // 反转全选按钮的状态      
    this.checkAll = !this.checkAll;     
    for (let i = 0; i < nodes.length; i++) {        
        const node = nodes[i];        
        // 触发当前cascader-menu的所有cascader-node组件的的doCheck方法,效果和背后逻辑与点击cascader-node是一样的。        
        if (!node.isDisabled) node.doCheck(this.checkAll, true);      
    }      
    // 触发cascader-panel的方法,这方法主要是更新checkValue的值。      
    panel.calculateMultiCheckedValue();    
},

通过上面的代码,只要用户点击了全选按钮,就能将该cascader-menu里的所有的cascader-node的状态更新。

到这里,全选按钮就能被渲染出来了,同时,点击全选按钮即可选中全部的cascader-node。

第三步:根据cascader-node的集合的check状态去更新全选按钮的状态

接下来有一个问题:

image.png

如果我此刻取消了东南,那么全选按钮是不是要切换为非全选呢?

这就需要根据cascader-menu的所有cascader-node的check状态来更新全选按钮的状态。

我们在cascader-menu.vue文件中写一个方法updateInDeterminate,根据cascader-node集合的check状态来更新全选按钮的状态。

updateInDeterminate() {
    const { panel, nodes } = this;
    if (panel.config.checkAll) {
    // 记录checked:true的数量
    let counter = 0;
    // 记录disabled:true的数量,就是不可以点击的cascader-node
    let disabledCounter = 0;
    // 记录非全选状态的cascader-node的数据量
    let indeterminateCounter = 0;
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (!node.isDisabled) {
        node.checked && counter++;
        node.indeterminate && indeterminateCounter++;
      } else {
        disabledCounter++;
      }
    }
    // 全选按钮的状态有下面两个计算出来
    this.checkAll = counter === (this.nodes.length - disabledCounter) && counter > 0;
    this.isIndeterminate = this.checkAll ? false : indeterminateCounter > 0 || counter > 0;
    }
},

而updateInDeterminate这个方法的触发时机是:

  1. cascader-menu创建时,在cascader-menu.vue中做修改
created() {
    if (this.panel.config.checkAll && this.index === 0) {    
        this.updateInDeterminate();      
        this.$on('updateInDeterminate', this.updateInDeterminate);    
    } 
},
  1. cascader-menu中传入的node参数变化时,在cascader-menu.vue中做修改
watch: {    
    nodes() {
      this.isCheckAll && this.updateInDeterminate();
    }
}
  1. cascader-panel组件中checkValue(这个指的是cascader选中的值)变化时,在cascader-panel中做修改
watch: {    
    checkedValue(val) {      
        if (!isEqual(val, this.value)) {       
             this.checkStrictly && this.calculateCheckedNodePaths();        
             // 此处触发        
             this.broadcast('ElCascaderMenu', 'updateInDeterminate');        
             this.$emit('input', val);        
             this.$emit('change', val);      
        }    
    }  
}

此处会报错,因为this.broadcast使用了eui自己写的事件触发方法,所以我们得在cascader-panel.vue文件中引入eui的emitter,并将emitter混入进去。

import emitter from 'element-ui/src/mixins/emitter';

export default {
  name: 'ElCascaderPanel',

  components: {
    CascaderMenu
  },

  mixins: [emitter],

还要在cascader-menu.vue中也干相同的操作

import emitter from 'element-ui/src/mixins/emitter';
export default {
  name: 'ElCascaderMenu',
  
  // 这里一定要添加这行
  componentName: 'ElCascaderMenu',

  mixins: [Locale, emitter]
  1. 删除cascader组件的tag时,在cascader.vue文件中修改
deleteTag(tag) {      
    // 此处添加panel
    const { checkedValue, panel } = this;      
    const current = tag.node.getValueByOption();      
    const val = checkedValue.find(n => isEqual(n, current));      
    this.checkedValue = checkedValue.filter(n => !isEqual(n, current));      
    // 如果全选功能开启了,那么删除tag时,就得触发updateInDeterminate函数      
    if (this.config.checkAll) {        
        this.$nextTick(() => {          
            panel.$refs.menu.forEach((menu) => {            
                menu.updateInDeterminate();          
            });       
        });      
    }      
    this.$emit('remove-tag', val);   
},

这样我们就能得到一下的结果:

录制_2022_03_14_15_08_56_711.gif

其实,只要在cascader-menu组件中通过deep监听node集合的check属性变化来触发updateInDeterminate函数就行了。但是这样会降低组件性能,所以没采用。

这种修改确实很难,如果哪一步实在不会的,可以加我微信:17688172759,我帮你远程。

看到最后的各位麻烦点个赞赞