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

81 篇文章 7 订阅

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

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

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

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


可以看:博客DEMO

起步

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

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

<!-- 在表头的tr里添加一个checkbox -->
 <th [nzChecked]="checked" [nzIndeterminate]="indeterminate" (nzCheckedChange)="onAllChecked($event)"></th>
<!-- 在表体的tr里添加一个checkbox -->
 <td [nzChecked]="setOfCheckedId.has(data.id)" (nzCheckedChange)="onItemChecked(data.id, $event)"></td>

有了这两行,我们就可以在表中看到一列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();

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

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

public all_checked = false;
public all_indeterminate = false;

继续:

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

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

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

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>

复选框状态的更改,完全通过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;
}

这个函数最简单,直接遍历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);
      })
    }
}

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

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();
  }

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

  • 首先定义了一个递归函数,递归的出口是该节点没有父节点时。然后每次都把当前节点的父节点进行递归操作。
  • 第一步,如果这个父节点有多个子节点
    • 那就判断这些子节点是不是都被选中了,然后就是三种状态了。
    • 值得注意的是,如果所有的子节点都没有被选中,那我们还要看下子节点的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;
      }
    }
  }

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

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

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

下课!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值