有点费解,为啥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状态,防止出现,子节点半选中了,但是父节点没有。refreshAllCheckedStatusrefreshAllCheckedStatus:
// 判断节点是否全部选中
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,至此为止,就完成了。
下课!