利用动态组件实现的ngx-table

zengrc

之前总结了一点对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

阅读 510
16 声望
0 粉丝
0 条评论
你知道吗?

16 声望
0 粉丝
宣传栏