Angular2 自定义 Grid(排序、分页、过滤、列模板、编辑模板)






4.60/5 (9投票s)
Angular2 自定义 Grid(排序、分页、过滤、列模板、编辑模板)
引言
当我开始我的第一个 Angular2 项目时,我很快意识到我需要一个自定义的 Grid 来满足我的项目需求。我需要以下功能,例如:
- 排序(开发者可以自行选择在哪列上启用排序功能)
- 过滤 (开发者可以自行选择在哪列上启用过滤功能)
- 分页 (开发者是否需要为 Grid 启用分页功能)
- 列模板(开发者可以为列指定要渲染的模板,例如,开发者可能需要两个操作按钮 EDIT 和 DELETE,或者任何其他按钮,例如:<div><button ...........> EDIT</button>)
- 编辑模板(开发者可以指定编辑模板)
您需要对 Angular2 有基本了解才能理解本文。解释每一行代码会使本文过于冗长,因此我将在这篇文章中尝试解释 Grid 的所有重要部分。
下载项目并将“cgrid.component.ts”文件添加到您的项目中。我已经为 Grid 的所有组件和服务创建了一个文件,这样就不需要管理多个文件了。
此文件将包含以下项目:
CCellDataService
:此服务将用于与 CGRID 组件以及我们将使用 CGRID 选择器的主组件进行通信。CGridSpinnerComponent
:此组件用于通过服务显示块状 UI。CGridCellComponent
:此组件用于加载动态模板,用于列模板和编辑模板,以指定 Grid 的自定义模板。Column
:用于指定列属性的类,例如标题的字段名、自定义模板、排序、过滤等。GridOption
:用于指定 Grid 属性的类,例如编辑模板、分页等。CGridComponent
:我们将为我们的 Grid 指定主模板的主组件。
理解代码
CGridSpinnerComponent 动态加载
对于这个 Grid,我们需要了解如何动态加载组件,甚至如何创建和加载组件。让我们看看 CGridSpinnerComponent
,我们通过 CCellDataService
在运行时加载它。
让我们创建 CGridSpinnerComponent
组件。
@Component({
selector: 'spinner',
styles: [
'.spinner-overlay { background-color: white; cursor: wait;}',
'.spinner-message-container { position: absolute; top: 35%; left: 0; right: 0; height: 0; text-align: center; z-index: 10001; cursor: wait;}',
'.spinner-message { display: inline-block; text-align: left; background-color: #333; color: #f5f5f5; padding: 20px; border-radius: 4px; font-size: 20px; font-weight: bold; filter: alpha(opacity=100);}',
'.modal-backdrop.in { filter: alpha(opacity=50); opacity: .5;}',
'.modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000;}'
],
template:
`<div class="in modal-backdrop spinner-overlay"></div>
<div class="spinner-message-container" aria-live="assertive" aria-atomic="true">
<div class="spinner-message" [ngClass]="spinnerMessageClass">{{ state.message }}</div>
</div>`
})
export class CGridSpinnerComponent {
state = {
message: 'Please wait...'
};
}
现在,此组件将通过服务使用“ComponentFactoryResolver
”进行加载。
此 ComponentFactoryResolver
将任何组件加载到“ViewContainerRef
”提供的父标签中。这个 viewcontainerref 将包含父标签对象的信息。假设我们想将组件加载到一个名为 gridloader 的 div 中,然后我们将创建一个该 div 的 viewcontainerref 对象。然后 ComponentFactoryResolver
将组件加载到该 div 中。以下方法(写在 CCellDataService
中)用于动态加载组件:
spinnerComp: ComponentRef<any>;
constructor(private _appRef: ApplicationRef, private _resolver: ComponentFactoryResolver) { }
public blockUI(placeholder) {
let elementRef = placeholder;
let factory = this._resolver.resolveComponentFactory(CGridSpinnerComponent);
this.spinnerComp = elementRef.createComponent(factory);
}
现在,在您想加载此 spinner 的主组件中,在模板中添加一个 div,例如“<div #gridLoader></div>
”。在 Angular2 应用程序中,我们可以创建 HTML 模板中任何标签的本地变量。并在主组件中创建一个该 div 的 viewcontainerref
对象,并使用 CCellDataService
调用上面的函数。
@ViewChild('gridLoader', { read: ViewContainerRef }) container: ViewContainerRef;
this.serviceObjectName.blockUI(container);
现在我们已经动态加载了组件,为了卸载,我们需要销毁该组件。此方法写在 CCellDataService
中。
public unblockUI() {
if (this.spinnerComp) {
this.spinnerComp.destroy();
}
}
调用主组件中的 unblockUI()
方法来卸载/销毁 spinner 组件。
CGridCellComponent 动态组件创建和动态加载
现在我们知道如何动态加载组件,就像我们上面为 CGridSpinnerComponent
. 所做的那样。现在我们将学习如何动态创建组件然后加载它,以及如何与这个动态创建的组件进行交互。
要创建组件,我们需要 Angular2 的以下内容:Compiler
、ViewContainerRef
、Component
类、ReflectiveInjector
、DynamicComponent
类、DynamicHtmlModule
类、ComponentFactory
、ModuleWithComponentFactories
等。让我们看看如何使用它们。
首先,我们将创建一个方法,该方法将为我们提供一个 ComponentFactory
的 promise 方法。在此方法中,我们将创建 DynamicComponent
类并分配要用于我的 Grid 单元格的数据、Grid 单元格使用的 eventemitter 等。
export function createComponentFactory(compiler: Compiler, metadata: Component, data:{}): Promise<ComponentFactory<any><componentfactory<any>> {
const cmpClass = class DynamicComponent {
row: {};
clickEvent: EventEmitter<{}>=new EventEmitter();
constructor() {
this.row = data;
}
onclick(customData:{}){
this.clickEvent.next(customData);
}
};
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [CommonModule, RouterModule], declarations: [decoratedCmp] })
class DynamicHtmlModule { }
return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
.then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
});
}
</any></componentfactory<any>
正如您所见,此方法将接受三个参数:1. Compiler:用于编译组件;2. Component:我们将用我们的 html 字符串创建的;3. data:object 类型,以便您可以向您的组件提供数据。在此函数中,我们正在创建一个 DynamicComponent
类对象。此类用于通过 EventEmitter
与父组件通信。因此,开发者可以使用 onclick()
函数来处理任何按钮,eventemitter 将与父组件通信数据。现在让我们看看如何在我们的 GridCellComponent
中使用此方法。
1. 首先创建 Component
类对象。
const compMetadata = new Component({
selector: 'dynamic-html',
template: this.htmlString// this is important, here we are providing the htmlstring dynamically to the componet.
});
2. 现在使用 createComponentFactory
promise 对象来创建和加载组件。
createComponentFactory(this.compiler, compMetadata, this.row)
.then(factory => {
const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
this.cmpRef.instance.clickEvent.subscribe(customData => {
this.fireClickEvent(customData);
});
});
请看步骤 2 中加亮的这部分:这里我们正在监听我们与 DynamicComponent
中的 eventemitter,在 createComponentFactory
函数中。
CGridComponent
这是我们的主组件,它将使用上述组件和服务与其他组件通信。此组件包含所有逻辑,如分页和事件发射器逻辑。主要部分是此组件的模板,该模板创建我们的 Grid 布局、分页布局等。
@Component({
selector: 'cgrid',
template: `<div style="width:100%">
<div style="height:90%">
<table class="table table-striped table-bordered table-hover table-condensed">
<thead>
<tr>
<th *ngFor="let col of gridOption.columns" style="background-color:red;">
<span *ngIf="!col.allowSorting">{{col.fieldName}}</span>
<span *ngIf="col.allowSorting && !(gridOption.currentSortField === col.field)" style="cursor:pointer;" (click)="onSort(col.field, 1)">
{{col.fieldName}}
<i class="fa fa-fw fa-sort"></i>
</span>
<span *ngIf="col.allowSorting && gridOption.currentSortField === col.field && gridOption.currentSortDirection == -1"
style="cursor:pointer;" (click)="onSort(col.field, 1)">
{{col.fieldName}}
<i class="fa fa-fw fa-sort-desc"></i>
</span>
<span *ngIf="col.allowSorting && gridOption.currentSortField === col.field && gridOption.currentSortDirection == 1"
style="cursor:pointer;" (click)="onSort(col.field, -1)">
{{col.fieldName}}
<i class="fa fa-fw fa-sort-asc"></i>
</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngIf="isFiltringEnabled()">
<td *ngFor="let col of gridOption.columns">
<input *ngIf="col.allowFiltering" type="text" #filter
[value]="getFiletrValue(col.field)"
(change)="onFilterChange(col.field, filter.value)" style="width:100%;">
</td>
</tr>
<tr *ngFor="let row of gridOption.data">
<ng-container *ngIf="!row['isEditing']">
<td *ngFor="let col of gridOption.columns" [style.width]="col.width">
<div *ngIf="col.isCustom">
<cgrid-cell [htmlString]="col.customTemplate" [row]="row"></cgrid-cell>
</div>
<div *ngIf="!col.isCustom">
{{ row[col.field] }}
</div>
</td>
</ng-container>
<ng-container *ngIf="row['isEditing']">
<td [attr.colspan]="3">
<cgrid-cell [htmlString]="gridOption.editTemplate" [row]="row"></cgrid-cell>
</td>
</ng-container>
</tr>
</tbody>
</table></div>
<div style="height: 10%;" class="text-right" *ngIf="gridOption.alloPaging">
<nav aria-label="Page navigation example">
<ul class="pagination pagination-sm justify-content-center">
<li class="page-item" [ngClass]= "isFirstPageDisabled()" (click)="onPageChange(1)">
<a class="page-link" aria-label="Previous">
<span aria-hidden="true">««</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item" [ngClass]= "isFirstPageDisabled()" (click)="onPageChange(gridOption.currentPage-1)">
<a class="page-link" aria-label="Previous" >
<span aria-hidden="true">«</span>
<span class="sr-only">Previous</span>
</a>
</li>
<li class="page-item" *ngFor="let page of getPageRange()" [ngClass]="{ 'active': page == gridOption.currentPage }" (click)="onPageChange(page)">
<a class="page-link" >{{page}}</a>
</li>
<li class="page-item" [ngClass]= "isLastPageDisabled()" (click)="onPageChange(gridOption.currentPage + 1)">
<a class="page-link" aria-label="Next" >
<span aria-hidden="true">»</span>
<span class="sr-only">Next</span>
</a>
</li>
<li class="page-item" [ngClass]= "isLastPageDisabled()" (click)="onPageChange(gridOption.totalPage)">
<a class="page-link" aria-label="Next" >
<span aria-hidden="true">»»</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
`,
styleUrls: []
})
CCellDataService
此服务用于与 CGridComponent
以及我们将使用 CGRID
的组件进行通信。此服务具有用于通信的 eventemitter。
@Injectable()
export class CCellDataService {
fireClickEmitter: EventEmitter<any> = new EventEmitter<any>();
fireClickEvent(data: {}) {
this.fireClickEmitter.next( data );
}
sortClickEmitter: EventEmitter<any> = new EventEmitter<any>();
sortClickEvent(data: {}) {
this.sortClickEmitter.next( data );
}
filterClickEmitter: EventEmitter<any> = new EventEmitter<any>();
filterClickEvent(data: {}) {
this.filterClickEmitter.next( data );
}
pageChangeEmitter: EventEmitter<any> = new EventEmitter<any>();
pageChangeEvent(data: {}) {
this.pageChangeEmitter.next( data );
}
spinnerComp: ComponentRef<any>;
constructor(private _appRef: ApplicationRef, private _resolver: ComponentFactoryResolver) { }
public blockUI(placeholder) {
let elementRef = placeholder;
let factory = this._resolver.resolveComponentFactory(CGridSpinnerComponent);
this.spinnerComp = elementRef.createComponent(factory);
}
public unblockUI() {
if (this.spinnerComp) {
this.spinnerComp.destroy();
}
}
}
使用代码
现在让我们看看如何在您的项目中实际使用此 CGRID。
- 此 Grid 使用 Bootstrap 类、字体和导航,因此首先在您的 index.html 中添加以下链接:
- https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css
- //maxcdn.bootstrap.ac.cn/font-awesome/4.1.0/css/font-awesome.min.css
- 在您的 app.module.ts 中导入以下内容:“
import { CGridComponent, CGridCellComponent, CGridSpinnerComponent } from './cgrid.component
'; - 在 app module 的 declarations 中添加以下内容:
declarations: CGridComponent, CGridCellComponent, CGridSpinnerComponent
。 - 在 app module 的 entryComponents 中添加以下声明:
CGridSpinnerComponent
。注意:所有我们动态加载的组件都需要添加到 entryComponents 中。 - 现在,在您的组件(我们称之为 ListComponnet)中,您想使用此 Grid 的地方,从该文件中导入组件和类。“
import { CGridComponent, CCellDataService, Column, GridOption } from './cgrid.component'
;” - 现在,在您想使用 cgrid 的地方,在模板中使用以下标签:
<cgrid [gridOption]="gridOption" ></cgrid> <!--gridOption is a input property of CGridComponnet, this will have the data, colums and other setting.-->
- 现在让我们在
ListComponnet
: 中创建 Grid 的数据:- 初始化
gridOption
类。gridOption: GridOption = new GridOption();
- 还要为 blockUI 创建一个
viewcontainerref
对象。@ViewChild("gridLoader", { read: ViewContainerRef }) container: ViewContainerRef;
- 现在创建列对象并将它们添加到
gridOption
对象中。let col1: Column = new Column(); col1.field = "title";// field from data col1.fieldName = "Title";// name of header col1.isCustom = false; col1.width = '240px'; col1.allowSorting = true; col1.allowFiltering = true; this.gridOption.columns.push(col1);
- 让我们为自定义 HTML 模板创建一个。
col1 = new Column(); col1.field = "Action"; col1.fieldName = "Action"; col1.isCustom = true; col1.width = '120px'; col1.customTemplate = "<table><tr><td><button type="button" class="btn btn-primary" (click)="onclick({\'data\' : row, \'control\': \'Edit\' })" [disabled]="row?.isDone">Edit</button>" col1.customTemplate = col1.customTemplate + "</td><td style="padding: 2px;">" col1.customTemplate = col1.customTemplate + "<button class="btn btn-danger" (click)="onclick({\'data\' : row, \'control\': \'Delete\' })">Delete</button></td></tr></table>" this.gridOption.columns.push(col1);
请注意加亮的 onclick 方法:在此方法中,我们可以传递自定义数据并可以在我们的
ListComponnet
中处理它,其中row
对象代表该行的数据。 - 现在让我们向
gridOption
对象添加一些数据。this.gridOption.data.push({'title': 'task1', 'isDone': false, 'isEditing': false});
请注意
isEditing
属性,此属性用于确定是否显示该行的编辑模板。目前将其设置为 false,我们将在下面解释编辑功能。所以,现在如果您加载您的应用程序,您将在 Grid 中看到结果。
- 初始化
-
现在让我们看看排序是如何工作的。
当您将列的
allowSorting
属性设置为true
时,当用户单击排序列时,会通过CCellDataService
生成一个事件。 监听排序事件的步骤:- 在
ListComponnet ngOnInit()
方法中订阅服务的sortClickEmitter
。this.ccelldataService.sortClickEmitter.subscribe((data) => { this.handleSorting(data); })
- 在
ListComponnet
中创建handleSorting
方法。在此方法中,数据对象将包含如下对象:{'field': sortField, 'direction': sortDirection}
,sortField
是您的列字段名,direction 是 1 或 -1,分别代表升序和降序。现在在handleSorting
中,像这样处理排序逻辑:handleSorting(data: {}){ let sortField: string = data['field']; let sortDirection: number = data['direction']; // sort your data according to the fiels and direction and reassign the data to this.gridOption.data this.gridOption.data = sorteddata; // now also assign the currentSortDirection and currentSortField this.gridOption.currentSortDirection = data['direction']; this.gridOption.currentSortField = data['field']; }
- 在
-
现在让我们看看过滤是如何工作的。
当您将列的
allowFiltering
属性设置为true
时,当用户在列的过滤输入框中输入文本时,会通过CCellDataService
生成一个事件。 监听过滤事件的步骤:- 在
ListComponnet ngOnInit()
方法中订阅服务的filterClickEmitter
。this.ccelldataService.filterClickEmitter.subscribe((data) => { this.handleFiltering(data); })
- 在
ListComponnet
中创建handleFiltering
方法。在此方法中,数据对象将包含如下对象:[{field: filterField, filterValue: filterValue}, {field: filterField, filterValue: filterValue}], filterField
是您的列字段名,filterValue
是输入框的值,像这样处理过滤逻辑:handleFiltering(data: {}){ // filter your data and reassign the data to this.gridOption.data this.gridOption.data = filtereddata; // now also assign the this.gridOption.filteredData this.gridOption.filteredData = data; }
- 在
-
现在让我们看看分页是如何工作的。
当您将 gridOption 的
this.gridOption.alloPaging = true;
属性设置为 true,并且也设置了 currentPage 和 total Page 属性,然后当用户单击分页图标时,会通过CCellDataService
生成一个事件。 监听分页事件的步骤:- 在
ListComponnet ngOnInit()
方法中订阅服务的pageChangeEmitter
。this.ccelldataService.pageChangeEmitter.subscribe((data) => { this.handlePaging(data); })
- 在
ListComponnet
中创建handlePaging
方法。在此方法中,数据对象将包含如下对象:{'currentPage': currentPage} 页码
,像这样处理分页逻辑:handlePaging(data: {}){ let pageNumber = data['currentPage'] //update the data according to this page number and reassign the data to this.gridOption.data this.gridOption.data = pageUpdatedData //also update the this.gridOption.currentPage this.gridOption.currentPage = pageNumber }
- 在
-
现在让我们看看按钮事件是如何工作的。
如何在步骤 7 中为列设置自定义模板,现在当用户单击任何事件时,会通过
CCellDataService
生成一个事件。 监听按钮触发事件的步骤:- 在
ListComponnet ngOnInit()
方法中订阅服务的fireClickEmitter
。this.ccelldataService.fireClickEmitter.subscribe((data) => { this.handleButtonEvent(data); })
- 在
ListComponnet
中创建handleButtonEvent
方法。在此方法中,数据对象将与您在自定义模板(步骤 7)中设置的一样,像这样处理按钮逻辑:handleButtonEvent(data: {}){ if(data['control'] === 'Delete'){ let tsk = data['data'];// row object this.ccelldataService.blockUI(this.container); // block UI this.updatedtasks = [ ]; for(let task of this.tasks ){ if(tsk['title'] != task.title){ this.updatedtasks.push(task); } } this.gridOption.data = this.updatedtasks; // update task this.ccelldataService.unblockUI() // unblock UI } }
- 在
-
现在让我们看看编辑模板是如何工作的。
假设您设置了一个 EDIT 按钮,并且在 EDIT 按钮上您想将该行设置为编辑模式,那么,按照步骤 11 处理 EDIT 事件,并添加以下逻辑:
if(data['control'] === 'Edit'){ let tsk = data['data'];// row object for(let task of this.tasks ){ if(tsk['title'] == task.title){ task['isEditing'] = true //////////////////////Important when you set this property to true the Cgrid will show the edit template of row } } this.gridOption.data = this.updatedtasks; // update task }
如何设置编辑模板?- 将
gridOption.editTemplate
属性设置为您的 html 字符串。this.gridOption.editTemplate = "html tags....<input #isdone type=text> <button (onclick)="onclick({\'data\' : row, \'control\': \'Update\',\'input\': isdone.value })" >"
按钮事件的处理方式与我们在自定义列模板中为 Edit/Delete 按钮所做的一样。
- 将
关注点
此控件可以修改和定制以添加更多功能。由于我自认为是一名 Angular2 的初学者,我的代码或我使用的方法离最优还很远,也不应被视为最佳实践,因此欢迎在此处发表任何评论。
历史
- 2017年3月6日:首次发布
- 2017年9月28日:添加了使用 cgrid 的示例。