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

0
18

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 following article (Thanks to Cory Shaw):

Migrating from Angular 1 to 2: Part 2, Components – Sortable Table

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 to read them before reading this article if you are new to Angular 2/4.  

As compare 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. are already have been explained in the previous articles.

Let’s Start

  1. Download the attached project from article, extract and open the solution file.
  2. 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 following screen:
  3. 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. Good thing is this is a data grid control that can be used for any kind of data where Export, Add/Edit/Delete buttons, their title and Search control visibility can easily be handled through input variables. Let’s first understand the UserComponents and see what variables we are sending to data controls, after that we will explore the DataGrid component.
UserComponent
  1. Edit the app->Components->user.component.ts.
  2. You can see we introduced the Data Grid variables that are self-explanatory. In initGridButton method, we are initiating the Header and Grid buttons, button object has title, key(s), action and ishide properties. Add the comma separated keys if you have more than one key to use e.g. keys: ['Id','DOB']. Why we are having initGridButton method and not assigning the values directly, we will see in future articles. This approach will help us to manage the read-only roles after reading the data from database. Do experiment to add more columns, change the sorting variable, add more header or grid buttons.

 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
 }

 ];

 }
 
  1. Another interesting method is gridaction subscribed to click event that is called when you click on any hdrbtns or gridbtns (Remember the Output variable). This has GridAction object as a parameter coming from DataGrid component that is filled in click method. The gridaction.values[0].value has first key value, if you have more than one key, you can get value like gridaction.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
  1. Edit the user.component.html from app->Components folder.
  2. You would see quite clean code, only one data-grid control to who we are sending the all variables we defined in UserComponent 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
  1. 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 find datagrid 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.
  2. The first import statement is self-explanatory, we have learned about Input, OutPut and EventEmitter decorators in previous articles. In second and third import statements, we are adding custom class DataGridUtil that has Export to Excel functionality and Format class having data format functions like Date, Text and CSV(comma separated) string. We will be looking into these classes in upcoming steps.
import { Component, Input, Output, EventEmitter} from '@angular/core';
import {DataGridUtil} from './datagrid.util'
import { Format } from './format';
  1. Next, we created one interface that contains action string (e.g.. Add, Update, Delete or whatever we are passing in gridbtns or hdrbtns object). We are sending GridAction interface as Output variable. If you see GridAction interface, we have action string and (key value pair) values variable, this is how it works. You will pass gridbtns collection with button title and key value(s) name (e.g.. primary or unique key(s)). When user clicks on any record’s Edit or Delete 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 upcoming steps:
export interface GridAction {
 action: string,
 values: {
 key: string,
 value: string
 }[]
}
  1. Next is the component meta data, the selector name, style sheet and html template. 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'
})
  1. 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:
    1. columns: any[]: Contains the list of columns to be displayed along title and format.
    2. data: any[]: The actual data to be displayed.
    3. sort: any: The column name and order (asc/desc) used to sort the data at the time of first load.
    4. gridbtns: any[]: Contains the list of buttons in grid (in our case, edit and delete) with button title, key(s) to be associated, hide/show and action string. Whatever action and key string values you will send in gridbtns object, it will be added in above given GridAction and send back to User Component where key value would be used for CRUD or any other operation.
    5. hdrbtns: any[]: Same description as gridbtns except it would be displayed on top of the grid, in our case it’s Add button.
    6. isshowfilter: boolean: Controls the Search bar display/hide.
    7. isExporttoCSV: boolean: Controls the Export to Excel button display/hide.
    8. exportFileName: string: Export to CSV file name that appends with current date and time.
    9. filter: any: The pipe object that would be used to filter the data. We have learned about pipe in previous article. 
  2. In Output decorator, we are defining one variable btnclick, that would emit the GridAction object every time any button in gridbtns or hdrbtns would be clicked. 
@Output() btnclick: EventEmitter<GridAction> = new EventEmitter<GridAction>();
  1. Next is three local variables to get the data clone and handle the Search functionality, this is same Search component we developed in previous article. The different is only we made it data grid part and getting the UserFilterPipe as input parameter to take care of its functionality. 
pdata: any[];
listFilter: string;
searchTitle: string = "Search:";
  1. Next we are implementing the ngOnChanges event that occurs when data is loaded in data variable. This data is assigned to local variable pdata 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 in data variable. The filtering or searching logic is defined in criteriaChange meted. 
ngOnChanges(changes: any) { if (JSON.stringify(changes).indexOf("data") != -1) this.pdata = this.data; this.criteriaChange(this.listFilter); }
  1. The selectedClass method is used to control the up/down icon on grid, changeSorting is called each time we click on sort icon. The changeSorting is bound to every column, it takes the column name, check if grid is previously sorted with same column, if yes, toggle the sort else store the clicked column name in sort variable that is being sent to orderby 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;
    }
  1. Next is click method that is being called every time any button in hdrbtns or gridbtns list is clicked. The parameters being passed from HTML templates are clicked btn object and current row. In function body, we are creating GridAction type variable, getting all keys from btn object, searching key(s) value from row object and pushing(adding) it in GridAction‘s values (key, value pair) variable. You can associate as many keys as you need for each button. I will show in upcoming steps how you can send keys 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); }
  1. 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 the transform method of input filter variable by passing the original data and input search string. That’s why I created the local variable pdata to keep the original data 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); } } }
  1. The next method is exporttoCSV, that takes the data variable and filters the specific columns values based on input column variable, calls the Format class transform method to properly format 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 to downloadcsv method of DataGridUtil 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
  1. Edit the datagrid.component.html from app->shared->datagrid folder. Let’s understand it step by step.
  2. Following code is for showing the Search control based on isshowfilter 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>
  1. Next code is to show header buttons and attaching the click event, we are looping through hdrbtns list, checking if ishide button is not true and passing the single hdrbtns item as a parameter to check the action, <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>
  1. Next, the entire table is for data grid, let’s understand the important parts:
    1. We are looping through the columns list, attaching the click event that calls the changeSorting method taking the data columns name as argument. Then looping through the gridbtns to create the empty td for action buttons (to keep the number of td 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>
  1. In tbody block, we are looping the actual data and applying the orderby filter that is getting parameter from convertSorting method. The convertSorting method is getting the sort column from input sort variable. In second loop, we are traversing the columns, getting the value of single column from each data row and also calling the format pipe at the same time. Remember, we are defining format with each column. In third loop, just like a header buttons, we are looping through the grid buttons, attaching the click event by providing the current button and current row (to get the key(s)) as arguments and also checking ishide 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>
DataGridUtil Class
  1. Edit the datagrid.util.ts from app->shared->datagrid folder.
  2. DataGridUtil has three functions that are quite self-explanatory, first one is downloadcsv that calls the converttoCSV method to convert data object to CSV and createFileName 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) { navigator.msSaveBlob(blob, this.createFileName(exportFileName)) } else { var link = document.createElement("a"); if (link.download !== undefined) { var url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", this.createFileName(exportFileName)); 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]) { row += index + ','; } row = row.slice(0, -1); 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
  1. Edit the format.cs from app->shared->datagrid folder.
  2. Format is the pipe implementing the PipeTransform interface. We are taking care of text, date and CSV data. Feel free to add more format if you need:
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
  1. Edit the app->shared->datagrid and edit the orderby.ts file.
  2. I took this pipe as it is from this article, so you can read it’s 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

Reference

Migrating from Angular 1 to 2: Part 2, Components – Sortable Table

LEAVE A REPLY