動態

詳情 返回 返回

利用動態組件實現的ngx-table - 動態 詳情

之前總結了一點對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}}&nbsp;&and;</div>
                <ng-template #desc>
                  <div *ngIf="checkSortStatus(th.id, 'desc'); else none" 
                        id='th.id' class="pointer" 
                        (click)="tableSort(th.id, 2)" >
                    {{th.name}}&nbsp;&or;</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="&lsaquo;" 
          nextText="&rsaquo;" 
          firstText="&laquo;" 
          lastText="&raquo;"></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

Add a new 評論

Some HTML is okay.