之前總結了一點對angular動態組件的理解,這裏將運用該特性製作一個可複用的Table控件。
背景
目前網上針對angular,有很多可以直接使用的UI以及控件框架,其中也包括Table控件,只需在html中使用定義的tag,並傳遞數據集以及其他等屬性值,就可以簡單創建一個Table;
但對於一些複製的表格,例如針對每行數據,最後一列有“view/edit/delete”按鈕的操作欄時,普通的Table控件無法滿足要求,只能直接使用原生的<table></table>實現;
由於沒有找到合適的Table控件可以滿足插入自定義的控件列,故這裏嘗試利用動態組件自己寫一個。
Pre-installation
npm install -g angular/cli
npm install -S ngx-bootstrap bootstrap
當table中數據集過大時,需要分頁,頁面導航使用ngx-bootstrap中的PaginationModule實現。
組件輸入
考慮可複用,Table接受:tableTitles,tableRows,paginationOptions作為輸入
///ngx-simple-table.component.ts
@Input() tableTitles: Array<{id:string,name:string,sort:boolean, type:number}>=[];
@Input() tableRows: Array<any>=[];
@Input() paginationOptions: {
totalItems: number,
maxSize: number,
itemsPerPage: number,
currentPage: number,
sort: string,
} = {
totalItems: 0,
maxSize: 5,
itemsPerPage: 10,
currentPage: 1,
sort: "0/#none",
}
- tableTitles是Table的列名:id用於對應數據集,使對應的列顯示在對應的title下;name為顯示名;sort表示其是否可排序;type用於分辨該列是否採用動態組件插入;
- tableRows是Table的數據集;
- paginationOptions為頁面導航屬性:totalItems表示數據集合總大小;maxSize表示導航欄顯示頁面總數;itemsPerPage表示每頁顯示數據條數;currentPage表示當前頁;sort表示當前排序方式;
組件輸出
組件與用户交互主要發生在三個時刻:
- 點擊列名排序
- 點擊頁面導航
- 點擊Table中動態組件內的按鈕等控件
由於Table中的動態組件隨着用户定義的不同,其中的行為邏輯也不同,故第三點交互過程在定義動態組件時實現,不在此處實現;
其餘兩處交互,定義:
///ngx-simple-table.component.ts
@Output() onPageChanged = new EventEmitter();
@Output() onSortClicked = new EventEmitter();
tableSort(...): void {
...
this.onSortClicked.emit(...);
}
pageChanged(...): void {
this.onPageChanged.emit(...);
}
在列名或頁面導航被點擊時,調用tableSort或pageChanged方法,傳入想要回傳的參數,利用emit方法發送回父組件即可,此處不詳述。
創建動態組件
首先,識別採用動態組件插入的列,記錄其Index:
identifiedIndex: {plainColumnIndex: Array<any>, spColumnIndex: Array<any>}
= {plainColumnIndex: [], spColumnIndex: []}
identifyColumn() {
let plainColumnIndex: Array<any>=[];
let spColumnIndex: Array<any>=[];
this.tableTitles.map((th,i)=>{
if(th.type == 0) plainColumnIndex.push(i);
else if(th.type == 1) spColumnIndex.push(i);
});
return {plainColumnIndex: plainColumnIndex, spColumnIndex: spColumnIndex}
}
ngOnInit() {
this.identifiedIndex = this.identifyColumn();
}
假設對於採用動態組件插入的列,其對應的tableRows數據集中,輸入格式為如下:
{component: Component, data: data}
eg:
[...
{
id: "000",
dueDate: "2018",
operations: {
component: ExampleComponent, ///ExampleComponent為自定義組件
data: "example",
}
}
...]
則在ngAfterViewInit中可以提取出該component,與data進行賦值,代碼如下:
///ngx-simple-table.component.ts
...
@ViewChildren('dynamicComponents',{read: ViewContainerRef}) public vcRefs: QueryList<ViewContainerRef>;
...
ngAfterViewInit() {
setTimeout(()=>this.generateSpItems(this.identifiedIndex.spColumnIndex));
this.spItemsHost.changes.subscribe((list) => {
setTimeout(()=>this.generateSpItems(this.identifiedIndex.spColumnIndex));
});
}
...
generateSpItems(spColumnIndex: Array<any>) {
let vcIndex = 0;
for(let rowIndex = 0; rowIndex < this.tableRows.length; rowIndex++){
for(let columnIndex = 0; columnIndex < spColumnIndex.length; columnIndex++){
let obj = this.tableRows[rowIndex][this.tableTitles[spColumnIndex[columnIndex]].id]
let spItem = obj.component;
let spData = obj.data;
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(spItem)
let vcRef = this.spItemsHost.toArray()[vcIndex];
vcIndex++;
vcRef.clear();
let spComponent = vcRef.createComponent(componentFactory);
(<any>spComponent.instance).data = spData;
}
}
}
- 因為需要插入動態組件的點有多個,此處使用的是ViewChildren,而非ViewChild;
- ViewChildren返回的集合為QueryList類型,該類型提供.changes.subscribe方法,用以監聽視圖更新後vcRefs的更新,此處vcRefs更新後,同步更新動態組件的插入;
- 動態組件的生成置於setTimeout中,是因為如果tableRows數據集合是來自http傳輸,即視圖初始化時,數據集同步更新,導致視圖更新的同時,數據集前後不一致,觸發ExpressionChangedAfterItHasBeenCheckedError報錯,使用setTimeout會將相應操作移後,使之不同步,參考這裏;如果數據集固定不變,則無需使用setTimeout;
- generateSpItems中使用雙重循環,是因為除了每行存在動態組件,一行中也可能存在複數動態組件;
- 每個循環生成一個動態組件spComponent 後,都進行了一次對其屬性data的賦值,這是因為動態組件不像普通組件能在.html中對其@Input賦值,故需要在此處賦值。
html模板
<!--ngx-simple-table.component.html-->
<table class="table">
<thead>
<tr>
<th>#</th>
<th *ngFor="let th of tableTitles">
<div *ngIf="!th.sort; else sort">{{th.name}}</div>
<ng-template #sort>
<div *ngIf="checkSortStatus(th.id, 'asc'); else desc"
id='th.id' class="pointer"
(click)="tableSort(th.id, 1)" >
{{th.name}} ∧</div>
<ng-template #desc>
<div *ngIf="checkSortStatus(th.id, 'desc'); else none"
id='th.id' class="pointer"
(click)="tableSort(th.id, 2)" >
{{th.name}} ∨</div>
</ng-template>
<ng-template #none>
<div id='th.id' class="pointer" (click)="tableSort(th.id, 0)" >
{{th.name}}</div>
</ng-template>
</ng-template>
</th>
</tr>
</thead>
<tbody *ngFor="let tr of tableRows; let trIndex = index">
<tr>
<th>{{(paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage + trIndex}}</th>
<td *ngFor="let th of tableTitles; let thIndex = index">
<div *ngIf="th.type == 0; else spComponent">{{tr[th.id]}}</div>
<ng-template #spComponent>
<div><ng-template #spItemsHost></ng-template></div>
</ng-template>
</td>
</tr>
</tbody>
</table>
<div class="justify-content-center">
<pagination
(pageChanged)="pageChanged($event)"
[(ngModel)]="paginationOptions.currentPage"
[boundaryLinks]="true"
[totalItems]="paginationOptions.totalItems"
[maxSize]="paginationOptions.maxSize"
[rotate]="false"
[itemsPerPage]="paginationOptions.itemsPerPage"
previousText="‹"
nextText="›"
firstText="«"
lastText="»"></pagination>
</div>
使用
在父組件中(記得在module中添加entryModule,加入動態組件TableOperationsComponent ):
///////parent.component.ts
...
import { TableOperationsComponent } from '.../table-operations.component'
...
export class parentComponent {
...
tableTitles = [
{id: "id", name: "Application No.", sort: true, type: 0},
{id: "submitT", name: "Submitted in", sort: true, type: 0},
{id: "operations", name: "", sort: false, type: 1},
]
applications: Array<any> = [{
id: '0000000000',
submitT: '2018',
operations: {component: TableOperationsComponent, data: {id: '0000000000', onDeleted: this.onDeleted.bind(this)}}
}];
paginationOptions = {
totalItems: 0,
maxSize: 5,
itemsPerPage: 10,
currentPage: 1,
sort: '0/#none',
}
onDeleted(id) {...}
...
}
//////parent.component.html
<ngx-simple-table
[tableTitles]="tableTitles"
[paginationOptions]="paginationOptions"
[tableRows]="applications">
</ngx-simple-table>
可以看到此處TableOperationsComponent作為通過@Input已經傳遞給ngx-simple-table對應的component了。
由於ngx-simple-table.component.ts創建組件後,同時傳入了data作為@input,故在TableOperationsComponent中:
...
@Input() data: {id:any, onDeleted: Function};
@Output() onDeleted = new EventEmitter();
ngOnInit() {
this.onDeleted.subscribe(this.data.onDeleted);
}
onDeleted(): void {
this.onDeleted.emit(this.data.id);
}
...
- 可以看到此處定義的@Input() data格式與傳入的operations.data格式相同,即動態組件的參數賦值可直接在父組件與動態組件間執行,而不需要在ngx-simple-table組件中進行;
- 對於@output,在ngOnInit時進行this.onDeleted.subscribe(this.data.onDeleted),則點擊刪除按鈕時,觸發this.onDeleted.emit,同時this.data.id作為參數發送給訂閲了emit事件的this.data.onDeleted,並調用該方法,從而實現相應操作;
- 注意到parent.component.ts中,輸入的onDeleted函數後使用了.bind(this)方法,這是因為onDeleted函數作為參數傳入動態組件後,上下文環境變化,如果不使用bind綁定,this的值將會發生改變。
總結
利用動態組件實現Table控件需要:
- 將動態組件作為@Input傳給Table控件;
- Table控件內實現CreateComponent,以及利用一個統一的{data: any}參數格式作為動態組件的@Input輸入;
- 動態組件加入entryModule;
- 動態組件定義@Input() data接收參數並根據邏輯使用;
- 對於動態組件需要調用外部方法的,定義@Output變量,利用subscribe以及emit方法,將需要處理的數據作為參數傳遞給父組件處理;
代碼
github