Angular 4 Data Grid with Sorting, Filtering & Export to CSV





5.00/5 (16 votes)
This article helps to understand the architecture and use of simple data grid developed in Angular 4.
Content
- Part 1: Angular2 Setup in Visual Studio 2017, Basic CRUD application, third party modal pop up control
- Part 2: Filter/Search using Angular2 pipe, Global Error handling, Debugging Client side
- Part 3: Angular 2 to Angular 4 with Angular Material UI Components
- Part 4: Angular 4 Data Grid with Export to Excel, Sorting and Filtering
Introduction
In this article, I am going to explain the simple data grid control with sorting, formatting and export to CSV functionality. I am assuming you also followed my previous Angular 2 & Angular 4 articles, if not, please go through them because I am going to take the previous article's project and add the data grid control in it.
The sorting and some formatting logic is taken from the following article (thanks to Cory Shaw):
Background
This would be a simple data grid control that will take data
, columns
, buttons
and few boolean variables to control the data grid layout. Since I am going to work on the same User Management
project from previous articles, I would highly recommend you read them before reading this article if you are new to Angular 2/4.
As compared to the previous articles, I am not going to explain step by step development because all concepts, e.g., Input
, Output
variables, Angular 4 If-Else
, etc. have already been explained in the previous articles.
Let's Start
- Download the attached project from the article, extract and open the
solution
file. - Rebuild the
solution
to download all NuGet and Client side packages for Angular, run the project and go to User Management screen. You should see the following screen: - This screen almost looks like the previous article, Angular 2 to Angular 4 with Angular Material UI Components project's screen except Export to Excel button and Sorting up/down icons. The good thing is this is a data grid control that can be used for any kind of data where
Export
,Add
/Edit
/Delete
buttons, theirtitle
andSearch
control visibility can easily be handled throughinput
variables. Let's first understand theUserComponents
and see what variables we are sending to data controls, after that, we will explore theDataGrid
component.
UserComponent
//Grid Vars start
columns: any[] = [
{
display: 'First Name',
variable: 'FirstName',
filter: 'text',
},
{
display: 'Last Name',
variable: 'LastName',
filter: 'text'
},
{
display: 'Gender',
variable: 'Gender',
filter: 'text'
},
{
display: 'Date of Birth',
variable: 'DOB',
filter: 'date'
}
];
sorting: any = {
column: 'FirstName',
descending: false
};
hdrbtns: any[] = [];
gridbtns: any[] = [];
initGridButton() {
this.hdrbtns = [
{
title: 'Add',
keys: [''],
action: DBOperation.create,
ishide: this.isREADONLY
}];
this.gridbtns = [
{
title: 'Edit',
keys: ['Id'],
action: DBOperation.update,
ishide: this.isREADONLY
},
{
title: 'X',
keys: ["Id"],
action: DBOperation.delete,
ishide: this.isREADONLY
}
];
}
//Grid Vars end
- Edit the
app->Components->user.component.ts
. - You can see that we introduced the Data Grid variables that are self-explanatory. In
initGridButton
method, we are initiating the Header and Grid buttons, button object hastitle
,key(s)
,action
andishide
properties. Add the comma separated keys if you have more than one key to use, e.g., keys:['Id','DOB']
. Why we are havinginitGridButton
method and not assigning the values directly, we will see in future articles. This approach will help us to manage theread-only
roles after reading the data from database. Do experiment to add more columns, change the sorting variable, add more header or grid buttons. - Another interesting method is
gridaction
subscribed toclick
event that is called when you click on anyhdrbtns
orgridbtns
(remember theOutput
variable). This hasGridAction
object as a parameter coming fromDataGrid
component that is filled inclick
method. Thegridaction.values[0].value
has first key value, if you have more than one key, you can get value likegridaction.values[1].value
and so on:gridaction(gridaction: any): void { switch (gridaction.action) { case DBOperation.create: this.addUser(); break; case DBOperation.update: this.editUser(gridaction.values[0].value); break; case DBOperation.delete: this.deleteUser(gridaction.values[0].value); break; } }
UserComponent Template
- Edit the user.component.html from
app->Components
folder. - You would see quite clean code, only one
data-grid
control to who we are sending all variables we defined inUserComponent
class:<data-grid [columns]="columns" [data]="users" [gridbtns]="gridbtns" [hdrbtns]="hdrbtns" [sort]="sorting" [isshowfilter]=true [isExporttoCSV]=true [exportFileName]="exportFileName" [filter] = userfilter (btnclick)="gridaction($event)"> </data-grid>
DataGrid Component
- Now that we saw what variables we are sending to Data Grid component, let's understand the data grid control, expand the
app->Shared
folder, you would finddatagrid
folder in it that contains all components and style sheet for data grid control. Expand this folder and double click on datagrid.component.ts file. - The first
import
statement is self-explanatory, we have learned aboutInput
,OutPut
andEventEmitter
decorators in previous articles. In the second and thirdimport
statements, we are adding custom classDataGridUtil
that hasExport to Excel
functionality andFormat
class having data format functions likeDate
,Text
andCSV
(comma separated)string
. We will be looking into these classes in the upcoming steps.
import { Component, Input, Output, EventEmitter} from '@angular/core';
import {DataGridUtil} from './datagrid.util'
import { Format } from './format';
- Next, we created one
interface
that contains action string (e.g..Add
,Update
,Delete
or whatever we are passing ingridbtns
orhdrbtns
object). We are sendingGridAction
interface asOutput
variable. If you seeGridAction
interface, we haveaction
string and (key value pair)values
variable, this is how it works. You will passgridbtns
collection with buttontitle
andkey value(s)
name (e.g.. primary or unique key(s)). When user clicks on any record'sEdit
orDelete
button,GridAction
with key name & value or list of key name & value (depends on how many keys you are sending) is sent back to parent component (User Component
), where it can be used to load existing record and update or delete operation. It will be clear to you in the upcoming steps:export interface GridAction { action: string, values: { key: string, value: string }[] }
- Next is the component
meta data
, theselector
name,style sheet
and htmltemplate
. Feel free to mess with style sheet if you don't like my simple grid:@Component({ selector: 'data-grid', styleUrls: ['app/shared/datagrid/datagrid.style.css'], templateUrl: 'app/shared/datagrid/datagrid.component.html' })
- After component's meta data, actual class body is starting. There are nine Input variables to load the data, adding the action buttons, enabling read-only, export to Excel and show search filter options:
columns: any[]
: Contains the list of columns to be displayed alongtitle
andformat
.data: any[]
: The actualdata
to be displayed.sort: any
: The column name and order (asc/desc) used to sort the data at the time of first load.gridbtns: any[]
: Contains the list of buttons in grid (in our case, edit and delete) with buttontitle
,key(s)
to be associated,hide/show
andaction
string. Whatever action and key string values you will send ingridbtns
object, it will be added in above givenGridAction
and send back toUser Component
where key value would be used for CRUD or any other operation.hdrbtns: any[]
: Same description asgridbtns
except it would be displayed on top of the grid, in our case it's Add button.isshowfilter: boolean
: Controls theSearch
bar display/hide.isExporttoCSV: boolean
: Controls theExport to Excel
button display/hide.exportFileName: string
:Export to CSV
file name that appends with current date and time.filter: any
: Thepipe
object that would be used to filter the data. We have learned aboutpipe
in previous article.
- In
Output
decorator, we are defining one variablebtnclick
, that wouldemit
theGridAction
object every time any button ingridbtns
orhdrbtns
would be clicked.@Output() btnclick: EventEmitter<GridAction> = new EventEmitter<GridAction>();
- Next is three local variables to get the
data
clone and handle theSearch
functionality, this is the sameSearch
component we developed in the previous article. The difference is only that we made it data grid part and getting theUserFilterPipe
as input parameter to take care of its functionality.pdata: any[]; listFilter: string; searchTitle: string = "Search:";
- Next, we are implementing the ngOnChanges event that occurs when data is loaded in
data
variable. This data is assigned to local variablepdata
because every time we do search, we still need original data, if we directly apply search filter on data variable, we lost the original data indata
variable. The filtering or searching logic is defined incriteriaChange
method.ngOnChanges(changes: any) { if (JSON.stringify(changes).indexOf("data") != -1) this.pdata = this.data; this.criteriaChange(this.listFilter); }
- The
selectedClass
method is used to control the up/down icon on grid,changeSorting
is called each time we click on sort icon. ThechangeSorting
is bound to every column, it takes the column name, check if grid is previously sorted with same column, if yes, toggle thesort
else store the clicked column name insort
variable that is being sent toorderby pipe
to perform sorting logic.selectedClass(columnName: string): any { return columnName == this.sort.column ? 'sort-' + this.sort.descending : false; } changeSorting(columnName: string): void { var sort = this.sort; if (sort.column == columnName) { sort.descending = !sort.descending; } else { sort.column = columnName; sort.descending = false; } } convertSorting(): string { return this.sort.descending ? '-' + this.sort.column : this.sort.column; }
- Next is the
click
method that is being called every time any button inhdrbtns
orgridbtns
list is clicked. The parameters being passed from HTMLtemplates
are clickedbtn
object and currentrow
. In function body, we are creatingGridAction
type variable, getting allkeys
frombtn
object, searchingkey(s) value
fromrow
object and pushing(adding) it inGridAction
's values (key, value pair) variable. You can associate as many keys as you need for each button. I will show in the upcoming steps how you can sendkeys
name:click(btn: any, row: any): void { let keyds = <GridAction>{}; keyds.action = btn.action; if (row != null) { keyds.values = []; btn.keys.forEach((key: any) => { keyds.values.push({ key: key, value: row[key] }); }); } this.btnclick.emit(keyds); }
- You might be familiar with
criteriaChange
method if you have read my previous articles, it is used for searching within the grid. We are explicitily calling thetransform
method of input filter variable by passing the originaldata
and inputsearch
string. That's why I created the local variablepdata
to keep the originaldata
variable, hopefully it is clear now:criteriaChange(value: any) { if (this.filter != null) { if (value != '[object Event]') { this.listFilter = value; this.pdata = this.filter.transform(this.data, this.listFilter); } } }
- The next method is
exporttoCSV
, that takes thedata
variable and filters the specific columns values based on inputcolumn
variable, calls theFormat
classtransform
method to properlyformat
the data as given in column variable (e.g., text, date, csv, etc.), after data to be exported object is fully loaded, it is being sent todownloadcsv
method ofDataGridUtil
class:exporttoCSV() { let exprtcsv: any[] = []; (<any[]>JSON.parse(JSON.stringify(this.data))).forEach(x => { var obj = new Object(); var frmt = new Format(); for (var i = 0; i < this.columns.length; i++) { let transfrmVal = frmt.transform(x[this.columns[i].variable], this.columns[i].filter); obj[this.columns[i].display] = transfrmVal; } exprtcsv.push(obj); } ); DataGridUtil.downloadcsv(exprtcsv, this.exportFileName); }
DataGrid Template
- Edit the
datagrid.component.html
fromapp->shared->datagrid
folder. Let's understand it step by step. - The following code is for showing the
Search
control based onisshowfilter
boolean input variable and data load. Rest is same as in the previous article, i.e., change event, etc.:<div *ngIf="isshowfilter && data"> <search-list [title]="searchTitle" (change)="criteriaChange($event)"></search-list> </div>
- Next code is to show
header
buttons and attaching theclick
event, we are looping throughhdrbtns
list, checking ifishide
button is not true and passing the singlehdrbtns
item as a parameter to check theaction
,<ng-container>
is a logical container that can be used to group nodes but is not rendered in the DOM tree as a node:<div *ngIf="data" class="add-btn-postion"> <div> <ng-container *ngFor="let hdrbtn of hdrbtns"> <button *ngIf="!hdrbtn.ishide" type="button" class="btn btn-primary" (click)="click(hdrbtn,null)">{{hdrbtn.title}}</button> </ng-container> <button *ngIf="isExporttoCSV && (data!=null && data.length>0)" type="button" class="btn btn-primary" (click)="exporttoCSV()">Export to Excel</button> </div> </div>
- Next, the entire table is for data grid, let's understand the important parts:
- We are looping through the columns list, attaching the
click
event that calls thechangeSorting
method taking the data columns name as argument. Then looping through thegridbtns
to create the emptytd
for action buttons (to keep the number oftd
even in header and dat rows).<tr> <th *ngFor="let column of columns" [class]="selectedClass(column.variable)" (click)="changeSorting(column.variable)"> {{column.display}} </th> <ng-container *ngFor="let btn of gridbtns"> <td *ngIf="!btn.ishide"></td> </ng-container> </tr>
- In
tbody
block, we are looping the actual data and applying theorderby
filter that is getting parameter fromconvertSorting
method. TheconvertSorting
method is getting thesort column
from inputsort
variable. In the second loop, we are traversing thecolumns
, getting the value of single column from each datarow
and also calling theformat pipe
at the same time. Remember, we are definingformat
with each column. In third loop, just like a header buttons, we are looping through the grid buttons, attaching theclick
event by providing the current button and current row (to get the key(s)) as arguments and also checkingishide
property to check either to show current button or not:<tr *ngFor="let row of pdata | orderby : convertSorting()"> <td *ngFor="let column of columns"> {{row[column.variable] | format : column.filter}} </td> <ng-container *ngFor="let btn of gridbtns"> <td *ngIf="!btn.ishide"> <button type="button" class="btn btn-primary" (click)="click(btn,row)">{{btn.title}}</button> </td> </ng-container> </tr>
- We are looping through the columns list, attaching the
DataGridUtil Class
- Edit the datagrid.util.ts from
app->shared->datagrid
folder. DataGridUtil
has three functions that are quite self-explanatory, first one isdownloadcsv
that calls theconverttoCSV
method to convertdata
object to CSV andcreateFileName
to append the date and time with file name to make it unique. This download functionality is tested in IE 11, Firefox and Chrome:public static downloadcsv(data: any, exportFileName: string) { var csvData = this.convertToCSV(data); var blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" }); if (navigator.msSaveBlob) { // IE 10+ navigator.msSaveBlob(blob, this.createFileName(exportFileName)) } else { var link = document.createElement("a"); if (link.download !== undefined) { // feature detection // Browsers that support HTML5 download attribute var url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", this.createFileName(exportFileName)); //link.style = "visibility:hidden"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } } private static convertToCSV(objarray: any) { var array = typeof objarray != 'object' ? JSON.parse(objarray) : objarray; var str = ''; var row = ""; for (var index in objarray[0]) { //Now convert each value to string and comma-separated row += index + ','; } row = row.slice(0, -1); //append Label row with line break str += row + '\r\n'; for (var i = 0; i < array.length; i++) { var line = ''; for (var index in array[i]) { if (line != '') line += ',' line += JSON.stringify(array[i][index]); } str += line + '\r\n'; } return str; } private static createFileName(exportFileName: string): string { var date = new Date(); return (exportFileName + date.toLocaleDateString() + "_" + date.toLocaleTimeString() + '.csv') }
Format Pipe
- Edit the format.cs from
app->shared->datagrid
folder. Format
is thepipe
implementing thePipeTransform
interface
. We are taking care oftext
,date
andCSV
data. Feel free to add more format if you need to:
export class Format implements PipeTransform {
datePipe: DatePipe = new DatePipe('yMd');
transform(input: any, args: any): any {
if (input == null) return '';
var format = '';
var parsedFloat = 0;
var pipeArgs = args.split(':');
for (var i = 0; i < pipeArgs.length; i++) {
pipeArgs[i] = pipeArgs[i].trim(' ');
}
switch (pipeArgs[0].toLowerCase()) {
case 'text':
return input;
case 'date':
return this.getDate(input);
case 'csv':
if (input.length == 0)
return "";
if (input.length == 1)
return input[0].text;
let finalstr: string = "";
for (let i = 0; i < input.length; i++) {
finalstr = finalstr + input[i].text + ", ";
}
return finalstr.substring(0, finalstr.length - 2);
default:
return input;
}
}
private getDate(date: string): any {
return new Date(date).toLocaleDateString();
}
}
OrderBy Pipe
- Edit the
app->shared->datagrid
and edit the orderby.ts file. - I took this
pipe
as it is from this article, so you can read its description from there.
Summary
This is a very simple data grid that can help you to show the formatted data with action buttons. Still, there is a lot of room for improvement, e.g., cascading, PDF export, in place editing (though I don't like it personally), pagination, etc. One more thing that I really want to enhance is to get rid of input filter
variable and make it generic for any data since we have enough information of input data, e.g., column name
and format
.
History
- Created on 7/9/2017