案例
这次还是主要拿之前分享的一片文章中出现的业务组建,上次只是贴了代码,并没有详细说出实现过程,这次就以这个业务组建为中心,讲述如何编写一个高性能的业务组件。
根据上图分析组件所要完成的功能
这个类似省市联动的加强版,可以查看被勾选的省市,并且复选框都有三个状态,未选、全选、未全选,默认状态只显示根数据,点击相应的选项,如果有子集就会显示对应的子集数据。
需求分解
这边先将需求分解,一步一步的来实现功能
- 实现省市联动。
- 实现全选功能,并且如果子集有未选项,父级状态变更为未选全。
- 实现被勾选项以标签的方式展示,并且标签带有移除功能,对应的复选框也要变更状态。
- 实现值获取。
第一步
根据观察可以可以使用二维数组,初始化时将整个根目录push到数组内,点击对应选项时,将子集push到数组内,以此类推。直接上代码。
Typescript:
@Component({
selector: 'directional-area-select',
exportAs: 'directionalAreaSelect',
templateUrl: './directional-select.component.html',
styleUrls: ['./directional-select.component.less']
})
export class DirectionalSelectComponent implements OnInit{
constructor() {
}
cacheList: any[] = [];
_inputList;
@Input('inputList') set inputList(value) {
if(value instanceof Array && value.length){
this._inputList = value;
this.inputListChange();
}
}
inputListChange() {
if (this._inputList instanceof Array && this._inputList.length > 0) {
this.cacheList.length = 0;
this.cacheList.push(this._inputList);
}
}
/**
* 显示对应的子集数据列表
* @param index1 当前层数下标
* @param index2 当前层数列表数据的下标
* @param list 当前层的列表数据
*/
pushCache(index1, index2, list) {
//往后选择
let cl = this.cacheList[index1 + 1];
let child = list[index2][this.child];
if (child instanceof Array && child.length > 0) {
if (!cl) {
this.cacheList.push(child);
} else {
if (cl !== child) {
this.cacheList.splice(index1 + 1, 1, child)
}
}
} else {
if (cl !== child && !(child instanceof Array)) {
this.cacheList.pop();
}
}
//往前选择
if (child && child.length > 0) {
while (this.cacheList.length > index1 + 2) {
this.cacheList.pop();
}
}
}
}复制代码
template:
<div class="select-list-inner">
<div class="scope" *ngFor="let list of cacheList;let index1 = index" [ngStyle]="{'width.%':100.0 / cacheList.length}">
<ul class="list-with-select">
<li class="spaui" *ngFor="let l of list;let index2 = index" (click)="pushCache(index1,index2,list)">
<app-checkbox [(ngModel)]="l.selected" [label]="l.name" [checkState]="l.checkState"></app-checkbox>
<i *ngIf="l[child]?.length > 0" class="icon yc-icon"></i>
</li>
</ul>
</div>
</div>
复制代码
逐步分析一下, @Input('inputList') set inputList(value) {}
,获取传入组件的值,即省市数据, inputListChange ,直接将整个数据push到 cacheList 里面。这边主要看看 pushCache 方法,用户操作时,有可能向前选择,也有可能向后选择,这边只要根据 cacheList 数组长度,和传进来的 index1 当前层数下标比较就能知道用户的操作。
往后选择也分三种情况
- 同层级操作列表未出现下一层子集
- 同层级操作列表以出现下一层子集
- 同层级操作列表并没有子集数据
第一种情况直接向 cacheList 数组push子集
第二种情况将对应层级的数据替换新的子集内容
第三种情况判断下层数据有值,并且对应层级列表没有子集内容,移除数组最后一项即可
往前选择直接判断 cacheList
长度和选择对应的层级下标来移除 cacheList
次数即可。
第二步
分析后,所有的选项都有复选框,所以每个都有肯能会有全选、未选、未全选的状态。
需增加三个方法
自身改变,也要将状态上下传递。
//选中有几个状态 对于父节点有 1全部选中 2部分选中 3全部取消 checkState 1 2 3
areaItemChange(data) {
let child = data[this.child];
if (data.selected) {
data.checkState = 1
} else {
data.checkState = 3
}
//向下寻找
if (child && child.length > 0) {
this.recursionChildCheck(child)
}
//向上寻找
this.recursionParentCheck(data);
}复制代码
通过递归的方式将子集的状态与父级状态同步
/**
* 同步子集和父级的状态
* 递归
* @param list
*/
private recursionChildCheck(list) {
if (list && list.length > 0) {
list.forEach(data => {
let checked = data.parent.selected;
data.selected = checked;
if (checked) {
data.checkState = 1;
data.selected = true;
} else {
data.checkState = 3;
data.selected = false;
}
let l = data[this.child];
this.recursionChildCheck(l)
})
}
}
复制代码
通过计算父级下子集的被选状态来确定父级最终状态,length 选中的个数,length2 部分选中的个数,通过一下比较就能确定父级的最终状态,一直递归到根元素。
/**
* 判断当前对象的父级中的子集被选中的个数和checkState == 2的个数来确定父级的当前状态
* 递归
* @param data
*/
private recursionParentCheck(data) {
let parent = data.parent;
if (parent) {
let l = parent[this.child];
let length = l.reduce((previousValue, currentValue) => {
return previousValue + ((currentValue.selected) ? 1 : 0)
}, 0);
let length2 = l.reduce((previousValue, currentValue) => {
return previousValue + ((currentValue.checkState == 2) ? 1 : 0)
}, 0);
if (length == l.length) {
parent.checkState = 1;
parent.selected = true;
} else if (length == 0 && length2 == 0) {
parent.checkState = 3
} else {
parent.checkState = 2;
parent.selected = false;
}
this.recursionParentCheck(parent);
}
}复制代码
需要更改一下 inputListChange 方法
list
inputListChange() {
if (this._inputList instanceof Array && this._inputList.length > 0) {
this.list = this._inputList.map(d => {
this.recursionChild(d);
return d;
});
this.cacheList.length = 0;
this.cacheList.push(this.list);
}
}
复制代码
/**
* 子集包含父级对象
* 递归
*/
private recursionChild(target) {
let list = target[this.child];
if (list && list.length > 0) {
list.forEach(data => {
data.parent = target;
this.recursionChild(data)
})
}
}
复制代码
这边为了方便操作,在子元素都创建一个parent字段保存父级内容。
第三步
获取被选的元素,将以标签的形式显示,如果父级的状态为全选,就不需要考虑子集,直接显示父级即可。
/**
* 获取被选的元素
* 父级状态为全选时,不需要考虑子集元素。
*/
private recursionResult(list, result = [], type = 1) {
if (list && list.length > 0) {
list.forEach(data => {
//全部选中并且父级没有复选框
if ((data[this.hasCheckbox] && data.checkState == 1) || data.checkState == 2) {
let child = data[this.child];
if (child && child.length > 0) {
this.recursionResult(child, result, type);
}
//全部选中并且父级有复选框 结果不需要包含子集
} else if (data.checkState == 1 && !data[this.hasCheckbox]) {
switch (type) {
case 1:
result.push(data.id);
break;
case 2:
result.push({
id: data.id,
name: data.name,
});
break;
case 3:
result.push(data);
break;
}
}
})
}
return result;
}
复制代码
标签移除方法
removeResultList(data) {
data.selected = false;
this.areaItemChange(data);
}
复制代码
需要更改 areaItemChange 方法,复选框改变都需要重新计算 resultList 的值,这样就达到了始终操作一个对象,改变对应标签状态,列表的状态也会跟着改变。
resultList
areaItemChange(data) {
if (data[this.hasCheckbox]) return;
let child = data[this.child];
if (data.selected) {
data.checkState = 1
} else {
data.checkState = 3
}
//向下寻找
if (child && child.length > 0) {
this.recursionChildCheck(child)
}
//向上寻找
this.recursionParentCheck(data);
this.resultList = this.recursionResult(this.list,[],3);
}
复制代码
第四步
获取的值其实就是标签里的内容 (。•ˇ‸ˇ•。) 。
进阶
可以改变组件的检查策略,将元数据 changeDetection 属性 设置为 ChangeDetectionStrategy.OnPush 只有输入属性改变才会触发检查。 值类型改变也会触发,引用类型只有引用改变才能触发检查。
可以将计算量比较大的代码另起一个后台线程来处理,就以递归绑定父元素为例子。
private recursionChild(target) {
let list = target[this.child];
if (list && list.length > 0) {
list.forEach(data => {
data.parent = target;
this.recursionChild(data)
})
}
}
/**
* 采用Worker
*/
private recursionChildWorker(target,fn = ()=>{}){
let fun = `
onmessage = function (e) {
let args = Array.from(e.data)
let list = args[0];
let key = args[1];
function parent(target){
let list = target[key];
if (list && list.length > 0) {
list.forEach(data => {
data.parent = target;
this.parent(data);
})
}
}
list.forEach(data => {
parent(data);
})
postMessage(list);
}
`;
const blob = new Blob([fun], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.postMessage([target, this.child]);
worker.onmessage = () => {
fn()
}
}
复制代码
完
至此,这个组件算是完成了,如果有更好的写法,欢迎留言,一起探讨 ^_^。