• 为AntDesign的Table组件(树形数据)添加Checkbox(NG-ZORRO)


    为AntDesign的Table组件(树形数据)添加Checkbox(NG-ZORRO)

    有点费解,为啥Ant-Design基于ReactVueTable组件都有为树形数据表格添加checkbox的示例,但是基于AngularNg-Zorro却没有。

    还是搞Angular的人太少啊,网上搜也搜不到类似的文章。大多数都是vue | react

    所以,我还是自己写个,也顺带理一下思路和逻辑。


    可以看:博客DEMO

    起步

    首先,我们要有一个树表(以下称为treeTable),这个NG-ZORRO中已经给了示例:NG-ZORRO的treeTable

    其次,我们要知道一个表格的Checkbox是怎么渲染上去的,就两行代码:

    
     <th [nzChecked]="checked" [nzIndeterminate]="indeterminate" (nzCheckedChange)="onAllChecked($event)">th>
    
     <td [nzChecked]="setOfCheckedId.has(data.id)" (nzCheckedChange)="onItemChecked(data.id, $event)">td>
    
    • 1
    • 2
    • 3
    • 4

    有了这两行,我们就可以在表中看到一列checkbook了。目前还不需要方法,所以方法调用可以去掉,能看到效果就行。

    至此为止,上述的东西,和官方文档中的一致,所以大家如果搞 × 了,自己看看文档😂。

    梳理

    如上述所说,直接把treeTablecheckbox这么强硬的结合,肯定是不行的。

    • 观察ng-zorro中给出的表格添加checkbox的逻辑不难得出,这个checkbox的逻辑针对的是一维数据的处理。
    • 处理时用到了Set类型来保证选择的唯一,同时checkboxchecked属性也可以根据Set中是否存在该值的唯一键key | id 等等来判断是否选中。
    • 针对于全选和反选以及半选中状态,则只需要判断下Set中是否全部包含数组数据,或者包含部分,或者完全不包含。

    PS:以下部分,话有点多,但是这是逻辑梳理部分,要简单看下。

    理解了一个普通表格添加checkbox的逻辑,我们再来考虑treeTable的:

    首先,treeTable的数据是层级分布的,那么就会出现几种情况:

    • 当选中父节点时,其所有子节点应该都要选中。

      我们得有一个方法onItemAndChildrenChecked去处理这个逻辑。

    • 当子节点选中时,父节点也会有对应的状态变化。

      • 如果父节点有多个子节点,那么选中一个子节点,所有父节点只能是半选中。
      • 如果父节点只有这一个子节点,那么父节点也应该同步选中。
      • 如果父节点的子节点全选中了或者全没选中,那么父节点也要选中或者取消选中。

      我们得有一个方法onItemParentChecked去处理这个逻辑。

    • 全选/全不选

      得有一个方法onAllChecked去处理这个逻辑

    • 要能在checkbox变化时,实时更新总checkbox的状态

      我们还得有一个方法refreshAllCheckedStatus去控制这个逻辑。

    OK,逻辑梳理完了

    剩下的就是写代码了。

    执行

    声明一下:写代码的过程中会大量用到mapOfExpandedData这个变量。

    这个变量的处理和定义,都是人家官方文档中给的示例里的,可别说没有啊!!!

    这个变量中主要就是把树形数据处理成了一维数据,可以打印看下。

    根据上面说的,传统的Set类型很明显已经不满足我们的数据处理要求了。

    我们需要的是一个能记录节点状态的东西,这里我选Map类型来做这个事情:

    public mapOfChecked: Map<string, { checked: boolean; indeterminate: boolean }> = new Map();
    
    • 1

    记录节点的选中状态和半选中状态。

    那同时,还需要有对应的全局checbox的变量:

    public all_checked = false;
    public all_indeterminate = false;
    
    • 1
    • 2

    继续:

    接着我们需要明确一下函数的调用,上一步说到,我们定义了四个函数onItemAndChildrenChecked onItemParentChecked onAllChecked refreshAllCheckedStatus

    onAllChecked 函数不必多说,给总的checkbox调用

    <th [nzChecked]="all_checked" [nzIndeterminate]="all_indeterminate" (nzCheckedChange)="onAllChecked($event)">th>
    
    • 1

    onItemAndChildrenCheckedonItemParentChecked函数都需要在单独的复选框触发时进行调用:

    <td
      [nzChecked]="!!mapOfChecked.get(item.key)?.checked"
      [nzIndeterminate]="!!mapOfChecked.get(item.key)?.indeterminate"
      (nzCheckedChange)="onItemAndChildrenChecked(mapOfExpandedData[data.key], item, $event);onItemParentChecked(mapOfExpandedData[data.key], item, $event)"
    >td>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    复选框状态的更改,完全通过mapOfChecked这个Map集合中存储的数据来判定。

    refreshAllCheckedStatus函数就是每次状态变化后进行一个判断,那就再onItemParentChecked函数的最后调用一下即可。

    继续:

    接下来就是分别这四个函数怎么写了:

    onAllChecked:

    // 全选/全不选
    onAllChecked(checked: boolean): void {
        Object.keys(this.mapOfExpandedData).forEach(item => {
          this.mapOfExpandedData[item]
            .forEach(({ key }) => this.mapOfChecked.set(key, { checked: checked, indeterminate: false }));
        });
        this.all_checked = checked;
        this.all_indeterminate = false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个函数最简单,直接遍历mapOfExpandedData,然后把每一个数据的选中状态都放入mapOfChecked这个Map即可。

    同时修改一下all_checkedall_indeterminate这个函数就完成了。

    onItemAndChildrenChecked:

    // 选中/非选中当前项及其子节点
    onItemAndChildrenChecked(array: TreeNodeInterface[], data: TreeNodeInterface, $event: boolean) {
        this.mapOfChecked.set(data.key, { checked: $event, indeterminate: false });
        if (data?.children) {
          data.children.forEach(item => {
            const target = array.find(el => el.key === item.key)!;
            this.onItemAndChildrenChecked(array, target, $event);
          })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个函数也简单,直接判断选中的节点有没有子节点,如果有,子节点也选中,然后递归完事。

    onItemParentChecked

      // 控制选中项的父节点半选中/不选中/选中
      onItemParentChecked(array: TreeNodeInterface[], data: TreeNodeInterface, $event: boolean) {
        const parentHalfCheck = (nodes: TreeNodeInterface) => {
          // 如果父节点有多个子节点
          if (nodes.children.length > 1) {
    
            // 判断子节点是否已经全部选中了
            let childrenNodesCheckLen = nodes.children.filter(item => !!this.mapOfChecked.get(item.key)?.checked);
    
            if (childrenNodesCheckLen.length) {
              if (childrenNodesCheckLen.length === nodes.children.length) {
                this.mapOfChecked.set(nodes.key, { checked: true, indeterminate: false });
              } else {
                this.mapOfChecked.set(nodes.key, { checked: false, indeterminate: true });
              }
            } else {
              let childrenNodesIndeterminateLen = nodes.children.filter(item => !!this.mapOfChecked.get(item.key)?.indeterminate);
              this.mapOfChecked.set(nodes.key, { checked: false, indeterminate: !!childrenNodesIndeterminateLen.length });
            }
          } else {
            // 如果父节点只有一个子节点,且子节点与点击的节点相同,那么父节点选中/不选中
            if (nodes.children[0].key === data.key) {
              this.mapOfChecked.set(nodes.key, { checked: $event, indeterminate: false });
            } else {
              // 如果父节点只有一个子节点,且子节点不同于点击的节点,则该节点要与子节点状态保持一致
              let children = this.mapOfChecked.get(nodes.children[0].key)
              this.mapOfChecked.set(nodes.key, { checked: children.checked, indeterminate: children.indeterminate });
            }
          }
          if (nodes?.parent) {
            const target = array.find(item => item.key === nodes.parent.key)!;
            parentHalfCheck(target);
          }
        }
    
        if (data?.parent) {
          parentHalfCheck(data.parent);
        }
    
        this.refreshAllCheckedStatus();
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    这个函数稍微有点麻烦,但是不要被吓到,我给简单解释下:

    • 首先定义了一个递归函数,递归的出口是该节点没有父节点时。然后每次都把当前节点的父节点进行递归操作。
    • 第一步,如果这个父节点有多个子节点
      • 那就判断这些子节点是不是都被选中了,然后就是三种状态了。
      • 值得注意的是,如果所有的子节点都没有被选中,那我们还要看下子节点的indeterminate状态,防止出现,子节点半选中了,但是父节点没有。
    • 第二步,如果子节点只有一个
      • 先看看这个父节点是不是选中节点的直接父节点
      • 如果不是,那就应该和子节点的状态保持一致即可。
    • 第三步,找到当前节点的 父节点,递归调用。
    • 第四步,调用refreshAllCheckedStatus

    refreshAllCheckedStatus:

      // 判断节点是否全部选中
      refreshAllCheckedStatus() {
        const mapOfExpandedDataKeys = Object.keys(this.mapOfExpandedData);
        const result = mapOfExpandedDataKeys.map(item => {
          const checkedLen = this.mapOfExpandedData[item].filter(el => this.mapOfChecked.get(el.key)?.checked);
          if (checkedLen.length) {
            if (checkedLen.length === this.mapOfExpandedData[item].length) {
              return 'ALL';
            } else {
              return 'HALF';
            }
          } else {
            return 'NONE';
          }
    
        });
        if (result.filter(x => x === 'ALL').length === mapOfExpandedDataKeys.length) {
          this.all_checked = true;
          this.all_indeterminate = false;
        } else {
          if (result.filter(x => x === 'NONE').length === mapOfExpandedDataKeys.length) {
            this.all_checked = false;
            this.all_indeterminate = false;
          } else {
            this.all_checked = false;
            this.all_indeterminate = true;
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这个函数逻辑比较简单点:

    • 遍历mapOfExpandedData数据,看看数据的状态
    • 如果ALL的长度和mapOfExpandedData的key长度一致,那就是全部选中了
    • 反之,HALF是半选,NONE不选中

    OK,至此为止,就完成了。

    下课!

  • 相关阅读:
    Nginx+Tomcat 搭建负载均衡、动静分离
    https转到http的nginx配置参考
    JVM垃圾回收总结(未完待续)
    【U8+】用友U8+客户端登录账套的时候, 提示: 已成功与服务器建立连接,但是在登录过程中发生错误; 指定的网络名不再可用。
    Cobalt Strike 注入msf会话
    Java贪心算法
    CentOS系统磁盘目录空间调整
    【鸿蒙 HarmonyOS 4.0】路由router
    Springboot企业生产报表管理系统的设计与实现7c063计算机毕业设计-课程设计-期末作业-毕设程序代做
    战备器材管理系统-部队物资仓库管理系统
  • 原文地址:https://blog.csdn.net/s_y_w123/article/details/127763823