为AntDesign的Table组件(树形数据)添加Checkbox(NG-ZORRO)
为AntDesign的Table组件(树形数据)添加Checkbox(NG-ZORRO)
有点费解,为啥Ant-Design基于React和Vue的Table组件都有为树形数据表格添加checkbox的示例,但是基于Angular的Ng-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了。目前还不需要方法,所以方法调用可以去掉,能看到效果就行。
至此为止,上述的东西,和官方文档中的一致,所以大家如果搞 × 了,自己看看文档😂。
梳理
如上述所说,直接把treeTable和checkbox这么强硬的结合,肯定是不行的。
- 观察
ng-zorro中给出的表格添加checkbox的逻辑不难得出,这个checkbox的逻辑针对的是一维数据的处理。 - 处理时用到了
Set类型来保证选择的唯一,同时checkbox的checked属性也可以根据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>
onItemAndChildrenChecked和onItemParentChecked函数都需要在单独的复选框触发时进行调用:
<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_checked和all_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,至此为止,就完成了。
下课!
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
