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
});
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";
col1.fieldName = "Title";
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 the row
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.
-
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:
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'];
this.gridOption.data = sorteddata;
this.gridOption.currentSortDirection = data['direction'];
this.gridOption.currentSortField = data['field'];
}
-
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:
-
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:
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']
this.gridOption.data = pageUpdatedData
this.gridOption.currentPage = pageNumber
}
-
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:
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'];
this.ccelldataService.blockUI(this.container);
this.updatedtasks = [ ];
for(let task of this.tasks ){
if(tsk['title'] != task.title){
this.updatedtasks.push(task);
}
}
this.gridOption.data = this.updatedtasks;
this.ccelldataService.unblockUI()
}
}
-
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'];
for(let task of this.tasks ){
if(tsk['title'] == task.title){
task['isEditing'] = true
}
}
this.gridOption.data = this.updatedtasks;
}
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.