Content
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, 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
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
}
];
}
- 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 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. - 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
- 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 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
- 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. - The first
import
statement is self-explanatory, we have learned about Input
, OutPut
and EventEmitter
decorators in previous articles. In the 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 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 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 the upcoming steps:
export interface GridAction {
action: string,
values: {
key: string,
value: string
}[]
}
- 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'
})
- 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 along title
and format
. data: any[]
: The actual data
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 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. hdrbtns: any[]
: Same description as gridbtns
except it would be displayed on top of the grid, in our case it's Add button. isshowfilter: boolean
: Controls the Search
bar display/hide. isExporttoCSV: boolean
: Controls the Export to Excel
button display/hide. exportFileName: string
: Export to CSV
file name that appends with current date and time. filter: any
: The pipe
object that would be used to filter the data. We have learned about pipe
in previous article.
- 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>();
- Next is three local variables to get the
data
clone and handle the Search
functionality, this is the same Search
component we developed in the previous article. The difference is only that 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:";
- 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
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. 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;
}
- Next is the
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 the 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);
}
- 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);
}
}
}
- 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
- Edit the
datagrid.component.html
from app->shared->datagrid
folder. Let's understand it step by step. - The 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>
- 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>
- 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 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>
- 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 the 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
- Edit the datagrid.util.ts from
app->shared->datagrid
folder. 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
- Edit the format.cs from
app->shared->datagrid
folder. 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 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
Reference