有点费解,为啥Ant-Design
基于React
和Vue
的Table
组件都有为树形数据表格添加checkbox
的示例,但是基于Angular
的Ng-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
了。目前还不需要方法,所以方法调用可以去掉,能看到效果就行。
至此为止,上述的东西,和官方文档中的一致,所以大家如果搞 × 了,自己看看文档。
如上述所说,直接把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
数据,看看数据的状态mapOfExpandedData
的key长度一致,那就是全部选中了HALF
是半选,NONE
不选中OK,至此为止,就完成了。
下课!