65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (9投票s)

2017年3月6日

CPOL

8分钟阅读

viewsIcon

46538

downloadIcon

1348

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

引言

当我开始我的第一个 Angular2 项目时,我很快意识到我需要一个自定义的 Grid 来满足我的项目需求。我需要以下功能,例如:

  • 排序(开发者可以自行选择在哪列上启用排序功能)
  • 过滤 (开发者可以自行选择在哪列上启用过滤功能)
  • 分页 (开发者是否需要为 Grid 启用分页功能)
  • 列模板(开发者可以为列指定要渲染的模板,例如,开发者可能需要两个操作按钮 EDIT 和 DELETE,或者任何其他按钮,例如:<div><button ...........> EDIT</button>
  • 编辑模板(开发者可以指定编辑模板)

您需要对 Angular2 有基本了解才能理解本文。解释每一行代码会使本文过于冗长,因此我将在这篇文章中尝试解释 Grid 的所有重要部分。

下载项目并将“cgrid.component.ts”文件添加到您的项目中。我已经为 Grid 的所有组件和服务创建了一个文件,这样就不需要管理多个文件了。

此文件将包含以下项目:

  1. CCellDataService:此服务将用于与 CGRID 组件以及我们将使用 CGRID 选择器的主组件进行通信。
  2. CGridSpinnerComponent:此组件用于通过服务显示块状 UI。
  3. CGridCellComponent:此组件用于加载动态模板,用于列模板和编辑模板,以指定 Grid 的自定义模板。
  4. Column:用于指定列属性的类,例如标题的字段名、自定义模板、排序、过滤等。
  5. GridOption:用于指定 Grid 属性的类,例如编辑模板、分页等。
  6. 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 的以下内容:CompilerViewContainerRefComponent 类、ReflectiveInjectorDynamicComponent 类、DynamicHtmlModule 类、ComponentFactoryModuleWithComponentFactories 等。让我们看看如何使用它们。

首先,我们将创建一个方法,该方法将为我们提供一个 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">&laquo;&laquo;</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">&laquo;</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">&raquo;</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">&raquo;&raquo;</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。

  1. 此 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
  2. 在您的 app.module.ts 中导入以下内容:“import { CGridComponent, CGridCellComponent, CGridSpinnerComponent } from './cgrid.component';
  3. 在 app module 的 declarations 中添加以下内容:declarations: CGridComponent, CGridCellComponent, CGridSpinnerComponent
  4. 在 app module 的 entryComponents 中添加以下声明:CGridSpinnerComponent。注意:所有我们动态加载的组件都需要添加到 entryComponents 中。
  5. 现在,在您的组件(我们称之为 ListComponnet)中,您想使用此 Grid 的地方,从该文件中导入组件和类。“import { CGridComponent, CCellDataService, Column, GridOption } from './cgrid.component';”
  6. 现在,在您想使用 cgrid 的地方,在模板中使用以下标签:
    <cgrid [gridOption]="gridOption" ></cgrid> <!--gridOption is a input property of CGridComponnet, this will have the data, colums and other setting.-->
    
  7. 现在让我们在 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 中看到结果。

  8. 现在让我们看看排序是如何工作的。

    当您将列的 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'];
      }
  9. 现在让我们看看过滤是如何工作的。

    当您将列的 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;
      }
  10. 现在让我们看看分页是如何工作的。

    当您将 gridOptionthis.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
          }
  11. 现在让我们看看按钮事件是如何工作的。

    如何在步骤 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
       }
      }
  12. 现在让我们看看编辑模板是如何工作的。

    假设您设置了一个 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 的示例。
© . All rights reserved.