Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Angular2 Custom Grid(Sorting, Paging, Filtering, Column template, edit template)

4.60/5 (9 votes)
27 Sep 2017CPOL8 min read 45.9K   1.3K  
Angular2 Custom Grid(Sorting, Paging, Filtering, Column template, edit template)

Introduction

As I started working on my first Angular2 project, soon I realized that I need a custom grid as per my project requirement. I need following functionality like:

  • Sorting (developer should choose on which columns he wants sorting functionality)
  • Filtering (developer should choose on which columns he wants filtering functionality)
  • Paging (developer wants paging or not for grid)
  • Column Template (developer can specify template to be render in a column for eg suppose developer wants two action buttons EDIT and DELETE buotton or any button, like: <div><button ...........> EDIT</button>)
  • Edit Template ( developer can specify the edit template)

Image 1

You need to have basic knowledge of Angular2, for this article. Explaing every bit of code will make this article lengthy, so I will try to explain all the important parts of the grid in this article.

Download the project and add "cgrid.component.ts" file to your project. I have created a single file for all the components and service of grid so that i don't have to manage multiple files.

This file will have following items:

  1. CCellDataService: This service will be used to comunicate with CGRID component and main component where we will use the CGRID selector.
  2. CGridSpinnerComponent: This component is used to show the block UI through service.
  3. CGridCellComponent: This component is used to load the dynamic template for column template and edit template used to specify the custom template of grid.
  4. Column: class to specify the column properties like fieldName for header, custom template, sorting, filtering etc.
  5. GridOption: class to specify the grid properties like edit template, paging etc,
  6. CGridComponent: Main component where we will specify the main template of our grid.

Understanding Code

CGridSpinnerComponent Dynamic Loading:

For this grid we need to understand how can we load a component dynamically and even creating and loading component. Let see the CGridSpinnerComponent, which we load at run time throug CCellDataService:

Lets create the CGridSpinnerComponent component:

JavaScript
@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...'
    };
}

Now this component will be loaded using "ComponentFactoryResolver" through service:

This ComponentFactoryResolver will load any component to the parent tag provided by "ViewContainerRef", this viewcontainerref will have parent tag object info, suppose we want to load the componet to a div with name gridloader, then we will create a viewcontainerref object of this div. Then ComponentFactoryResolver will load the component to this div. This following method(written in CCellDataService) is used to load a component dynamicall:

JavaScript
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);
}

Now in main componnet where you want to laod this sppiner add a div in the template like "<div #gridLoader></div> " in Angular2 application we can create the loacal variable of any tag in html template. and create a viewcontainerref object of this div in main component and call the above function using CCellDataService:

JavaScript
@ViewChild('gridLoader', { read: ViewContainerRef }) container: ViewContainerRef;
 this.serviceObjectName.blockUI(container);

Now as we loaded the component dynamically, for unloading we need to destroy the component. this method is wrriten in CCellDataService:

JavaScript
public unblockUI() {
     if (this.spinnerComp) {
         this.spinnerComp.destroy();
     }
 }

call the unblockUI() method in main component to unload/destroythe spinner component.

CGridCellComponent dynamic component creation and dynamic loading:

Now we know how to load a componnet dynamically as we did above for CGridSpinnerComponent. Now we will learn how to dynamically create the componnet and then load the same, and how can we interact with this dynamically created component:

To create the component we need forlowing from Angular2: Compiler, ViewContainerRef, Component class, ReflectiveInjector, DynamicComponent class, DynamicHtmlModule class, ComponentFactory, ModuleWithComponentFactories etc. Lets see how to use them:

First we will create a method which will provide us a promise method of ComponentFactory, in this method we will create the DynamicComponent class and assign things like data to be used in my grid cell, eventemitter used by grid cell etc.

JavaScript
    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>

As you can see this method will take three argumets 1. Compiler: used to compile the component 2. Component: which we will create with our html string and 3, data: of object type so that you can provide data to your component. In this function we are creating a DynamicComponent class object, this class is used to comunicate with parent componet with use of EventEmitter. So dev can use the onclick() function with any button and the eventemitter will comunicate the data with parent component. Now lets see how to use this method in our GridCellComponent:

1. First create the Component class object:

JavaScript
const compMetadata = new Component({
    selector: 'dynamic-html',
    template: this.htmlString// this is important, here we are providing the htmlstring dynamically to the componet.
});

2. Now use the createComponentFactory promise object to create and load the componet:

JavaScript
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);
    });
});

Have a look at the highlighted part in step 2, here we are listening to the eventemitter which we create in the DynamicComponent, in function createComponentFactory.

CGridComponent

This is our main component which will use the above components and the service to comunicate with other component. This componnet has all the logic like paging and evintemiter logics. The main part is the template of this component, this template creates our grid layout, aging layout etc:

JavaScript
@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

This service is used to comunicate with the CGridComponent and the componnet in which we will use the CGRID. This service have eventemiter to comunicate:

JavaScript
@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();
        }
    }
}

Using the code

Now lets see how to use this CGRID in your project:

  1. This grid uses bootstrap classes and fonts and navigation, so first add following links to your index.html:
    • https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css
    • //maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css
  2. import following to your app.module.ts: "import { CGridComponent, CGridCellComponent, CGridSpinnerComponent } from './cgrid.component';
  3. in app module declaration add following declarations: CGridComponent, CGridCellComponent, CGridSpinnerComponent.
  4. In app module entryComponents add following declaration: CGridSpinnerComponent. NOTE: all those component which we load dynamically need to be added to entryComponents.
  5. Now in your component(lets name it ListComponnet), where you want to use this grid imports componnets and classes from this file. "import { CGridComponent, CCellDataService, Column, GridOption } from './cgrid.component';"
  6. Now use following tag in your template where you want to use cgrid:
    <cgrid [gridOption]="gridOption" ></cgrid> <!--gridOption is a input property of CGridComponnet, this will have the data, colums and other setting.-->
  7. Now lets create the data for grid in ListComponnet:
    • initialize gridOption class :
      JavaScript
      gridOption: GridOption = new GridOption();
    • also create a viewcontainerref object for blockUI:
      JavaScript
      @ViewChild("gridLoader", { read: ViewContainerRef }) container: ViewContainerRef;
    • Now create objects of column and add those to gridOption object:
      JavaScript
      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);
    • Lets create one for custom html template:
      JavaScript
      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);

      Please note the highlighted onclick method: In this method we can pass our custom data and can handle the same in our ListComponnet, in this the row object represent the data for row.

    • Now lets add some data to the gridOption object:
      JavaScript
      this.gridOption.data.push({'title': 'task1', 'isDone': false, 'isEditing': false});

      Please note the isEditing property, this property is used whether you want to show the edit template for the row. For now assign false to it, we explain editing below.

      So, now if you load your application then you will see the result in grid.

  8. Now let see how sorting works:

    When you set the allowSorting property to true of the column then when user click to sort the column an event is generated through CCellDataService. steps to listen to sort event:

    • in ListComponnet ngOnInit() method subscribe to the sortClickEmitter of service:
      JavaScript
      this.ccelldataService.sortClickEmitter.subscribe((data) => {
          this.handleSorting(data);
      })
    • create handleSorting method in ListComponnet in this method the data object will have object like this: {'field': sortField, 'direction': sortDirection}, sortField is the name of your column field name and direction is either 1 or -1 for assending and descending. Now in handleSorting, handle the sorting logic like this:
      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. Now let see how filtering works:

    When you set the allowFiltering property to true of the column then when user enter the text to filter input of column an event is generated through CCellDataService. steps to listen to filter event:

    • in ListComponnet ngOnInit() method subscribe to the filterClickEmitter of service:
      JavaScript
      this.ccelldataService.filterClickEmitter.subscribe((data) => {
          this.handleFiltering(data);
      })
    • create handleFiltering method in ListComponnet in this method the data object will have object like this: [{field: filterField, filterValue: filterValue}, {field: filterField, filterValue: filterValue}], filterField is the name of your column field name and filterValue is value of input, handle the filtering logic like this:
      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. Now let see how Pagination works:

    When you set the this.gridOption.alloPaging = true; property of the gridOption, and also sets the currentPage and total Page property, then when user click on pageing icons an event is generated through CCellDataService. steps to listen to page event:

    • in ListComponnet ngOnInit() method subscribe to the pageChangeEmitter of service:
      JavaScript
      this.ccelldataService.pageChangeEmitter.subscribe((data) => {
          this.handlePaging(data);
      })
    • create handlePaging method in ListComponnet in this method the data object will have object like this:{'currentPage': currentPage} the page number, handle the paging logic like this:
      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. Now let see how button event works:

    How to set custom template to column explained in step 7, now when user click on any event an event is generated through CCellDataService. steps to listen to button fire event:

    • in ListComponnet ngOnInit() method subscribe to the fireClickEmitter of service:
      JavaScript
      this.ccelldataService.fireClickEmitter.subscribe((data) => {
          this.handleButtonEvent(data);
      })
    • create handleButtonEvent method in ListComponnet in this method the data object will have object like you set in your custom template(step 7), handle the button logic like this:
      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. Now let see how Edit template works:

    Suppose you set an EDIT button, and on EDIt button you want to set the row in edit mode then, handle the EDIT event according to step 11, and add following logic:

    JavaScript
    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
     }
    How to set the edit template:
    • set the gridOption.editTemplate property to your html string
      this.gridOption.editTemplate = "html tags....<input #isdone type=text>       <button (onclick)="onclick({\'data\' : row, \'control\': \'Update\',\'input\': isdone.value })" >"

      The button events are handled in same way like we did for Edit/Delete button in custom column template.

Points of Interest

This control can be modified and customized to add more features. As I consider myself a beginner in Angular2, my code or the methods that I am using are far from optimum and not to be considered as a best practice so any comments are welcome here.

History

  • 6th March, 2017: First post
  • 28 Sept, 2017: added example for using cgrid.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)