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

之前总结了一点对angular动态组件的理解,这里将运用该特性制作一个可复用的Table控件。

背景

目前网上针对angular,有很多可以直接使用的UI以及控件框架,其中也包括Table控件,只需在html中使用定义的tag,并传递数据集以及其他等属性值,就可以简单创建一个Table;

但对于一些复制的表格,例如针对每行数据,最后一列有“view/edit/delete”按钮的操作栏时,普通的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=[]; 
  @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, spColumnIndex: Array} 
  = {plainColumnIndex: [], spColumnIndex: []}

  identifyColumn() {
      let plainColumnIndex: Array=[];
      let spColumnIndex: Array=[];
      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;
...
  ngAfterViewInit() {
    setTimeout(()=>this.generateSpItems(this.identifiedIndex.spColumnIndex));
    this.spItemsHost.changes.subscribe((list) => {
      setTimeout(()=>this.generateSpItems(this.identifiedIndex.spColumnIndex));
    });
  }
...
  generateSpItems(spColumnIndex: Array) {
    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);
        (spComponent.instance).data = spData;
      }
    }
  }
  • 因为需要插入动态组件的点有多个,此处使用的是ViewChildren,而非ViewChild;
  • ViewChildren返回的集合为QueryList类型,该类型提供.changes.subscribe方法,用以监听视图更新后vcRefs的更新,此处vcRefs更新后,同步更新动态组件的插入;
  • 动态组件的生成置于setTimeout中,是因为如果tableRows数据集合是来自http传输,即视图初始化时,数据集同步更新,导致视图更新的同时,数据集前后不一致,触发ExpressionChangedAfterItHasBeenCheckedError报错,使用setTimeout会将相应操作移后,使之不同步,参考这里;如果数据集固定不变,则无需使用setTimeout;
  • generateSpItems中使用双重循环,是因为除了每行存在动态组件,一行中也可能存在复数动态组件;
  • 每个循环生成一个动态组件spComponent 后,都进行了一次对其属性data的赋值,这是因为动态组件不像普通组件能在.html中对其@Input赋值,故需要在此处赋值。

html模板


       
#
{{th.name}}
{{th.name}} ∧
{{th.name}} ∨
{{th.name}}
{{(paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage + trIndex}}
{{tr[th.id]}}

使用

在父组件中(记得在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 = [{
    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
    
    

可以看到此处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

你可能感兴趣的:(angular2,angular4)