之前总结了一点对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方法,将需要处理的数据作为参数传递给父组件处理;