案例
這次還是主要拿之前分享的一片文章中出現的業務組建,上次只是貼了程式碼,並沒有詳細說出實現過程,這次就以這個業務組建為中心,講述如何編寫一個高效能的業務元件。
根據上圖分析元件所要完成的功能
這個類似省市聯動的加強版,可以檢視被勾選的省市,並且核取方塊都有三個狀態,未選、全選、未全選,預設狀態只顯示根資料,點選相應的選項,如果有子集就會顯示對應的子集資料。
需求分解
這邊先將需求分解,一步一步的來實現功能
- 實現省市聯動。
- 實現全選功能,並且如果子集有未選項,父級狀態變更為未選全。
- 實現被勾選項以標籤的方式展示,並且標籤帶有移除功能,對應的核取方塊也要變更狀態。
- 實現值獲取。
第一步
根據觀察可以可以使用二維陣列,初始化時將整個根目錄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()
}
}
複製程式碼
完
至此,這個元件算是完成了,如果有更好的寫法,歡迎留言,一起探討 ^_^。