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






4.60/5 (9 votes)
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)
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:
CCellDataService
: This service will be used to comunicate with CGRID component and main component where we will use the CGRID selector.CGridSpinnerComponent
: This component is used to show the block UI through service.CGridCellComponent
: This component is used to load the dynamic template for column template and edit template used to specify the custom template of grid.Column
: class to specify the column properties like fieldName for header, custom template, sorting, filtering etc.GridOption
: class to specify the grid properties like edit template, paging etc,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:
@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:
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
:
@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
:
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.
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:
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:
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:
@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
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:
@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:
- 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
- import following to your app.module.ts: "
import { CGridComponent, CGridCellComponent, CGridSpinnerComponent } from './cgrid.component
'; - in app module declaration add following
declarations: CGridComponent, CGridCellComponent, CGridSpinnerComponent
. - In app module
entryComponents
add following declaration:CGridSpinnerComponent
. NOTE: all those component which we load dynamically need to be added to entryComponents. - 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'
;" - 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.-->
- Now lets create the data for grid in
ListComponnet
:- initialize
gridOption
class :gridOption: GridOption = new GridOption();
- also create a
viewcontainerref
object for blockUI:@ViewChild("gridLoader", { read: ViewContainerRef }) container: ViewContainerRef;
- Now create objects of column and add those to
gridOption
object: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:
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 therow
object represent the data for row. - Now lets add some data to the
gridOption
object: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.
- initialize
-
Now let see how sorting works:
When you set the
allowSorting
property totrue
of the column then when user click to sort the column an event is generated throughCCellDataService
. steps to listen to sort event:- in
ListComponnet ngOnInit()
method subscribe to thesortClickEmitter
of service:this.ccelldataService.sortClickEmitter.subscribe((data) => { this.handleSorting(data); })
- create
handleSorting
method inListComponnet
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 inhandleSorting
, 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']; }
- in
-
Now let see how filtering works:
When you set the
allowFiltering
property totrue
of the column then when user enter the text to filter input of column an event is generated throughCCellDataService
. steps to listen to filter event:- in
ListComponnet ngOnInit()
method subscribe to thefilterClickEmitter
of service:this.ccelldataService.filterClickEmitter.subscribe((data) => { this.handleFiltering(data); })
- create
handleFiltering
method inListComponnet
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 andfilterValue
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; }
- in
-
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 throughCCellDataService
. steps to listen to page event:- in
ListComponnet ngOnInit()
method subscribe to thepageChangeEmitter
of service:this.ccelldataService.pageChangeEmitter.subscribe((data) => { this.handlePaging(data); })
- create
handlePaging
method inListComponnet
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 }
- in
-
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 thefireClickEmitter
of service:this.ccelldataService.fireClickEmitter.subscribe((data) => { this.handleButtonEvent(data); })
- create
handleButtonEvent
method inListComponnet
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 } }
- in
-
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:
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 stringthis.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.
- set the
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.