为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>

有了这两行,我们就可以在表中看到一列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,至此为止,就完成了。

下课!


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部