Displaying data in a tabular form a.k.a. in a Grid layout is a very common requirement. The HTML table element is the most suitable UI for implementing such functionality requirements.
Usually users who have used DataGrid / GridView or similar controls, expect that the HTML table should provide features like CRUD operations so that no additional form needs to be created for manipulating data.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering Angular, React, .NET Core, MVC, Azure and more. Subscribe to our magazine for FREE and download all previous, current and upcoming editions.
This article demonstrates how to use an HTML Table as a Grid for CRUD operations using Angular ng-template.
Pre-requisites
To understand this article, the reader must have knowledge of Node.js, Express and basics of Angular with module loader e.g. Systemjs and package.json.
If you are new to these technologies, check our tutorial series at https://www.dotnetcurry.com/javascript/1226/learn-nodejs-es6-jquery-angularjs-html5
Angular ng-template
This is Angular’s own implementation of Web Component’s <template> concept introduced a few years ago. It is a new tag provided in Angular 4 which deprecates <template> tag in Angular 2. This tag is used to render a reusable HTML structure for e.g. on the click event of the button, a read-only table row can be replaced by the table row containing input text elements.
Angular 4 Grid - Implementation
The application is implemented using Visual Studio Code IDE. This application contains REST APIs developed using Express, a Data Access Layer implemented using Mongoose Driver and Angular for front-end application.
The following diagram provides the architecture of the implementation
To implement REST API, please follow the steps provided at https://www.dotnetcurry.com/aspnet-mvc/1135/using-nodejs-express-mongodb-meanstack-in-aspnet-mvc to setup the MongoDB for creating database and collections.
This link covers the creation of REST APIs using Express. Download or follow the code of the article to create the REST API.
Open the dal.js from the downloaded code and add the following line in it to use native promises of the mongoose driver for connecting to MongoDB.
mongooseDrv.Promise = global.Promise;
Later modify the code in Step 3 below for adding update and delete functions as shown in the following code:
exports.update = function (request, response) {
var id = request.params.Id;
var updEmp = {
EmpNo: request.body.EmpNo,
EmpName: request.body.EmpName,
Salary: request.body.Salary,
DeptName: request.body.DeptName,
Designation: request.body.Designation
};
console.log(" in update id " + id + JSON.stringify(updEmp));
EmployeeInfoModel.update({
_id: id
}, updEmp, function (remError, updatedEmp) {
if (remError) {
response.send(500, {error: remError});
} else {
response.send({success: 200});
}
});
};
exports.delete = function (request, response) {
var id = request.params.Id;
console.log("id " + id);
EmployeeInfoModel.remove({
_id: id
}, function (remError, addedEmp) {
if (remError) {
response.send(500, {error: remError});
} else {
response.send({success: 200});
}
});
};
The update function reads the Id parameter from the request header. The request body parameters are read to create a updEmp object which is used for updating Employee record. The update function of EmployeeInfoModel object updates the Employee record by searching the record based on the Id parameter and then updates the Employee information using updEmp object.
The delete function reads the Id parameter from the request header and by calling the remove function of the EmployeeInfModel, the record is deleted.
In the app.js add the following two lines to call put() and delete() functions of the Express object.
app.put('/EmployeeList/api/employees/:Id', rwOperation.update);
app.delete('/EmployeeList/api/employees/:Id', rwOperation.delete);
The put() and delete() functions accept two parameters, the first is the URL of REST API and second is the callback function performing the request processing for update and delete operations.
This completes the REST API.
As per the diagram discussed above, the REST API and data access layer is hosted in the Node.js runtime.
Implementing an Angular Front-End application
Step 1: Create a folder of the name NG4DataGridE2E_Client_Node of the disk. Open VS Code and open this folder in it. This folder will be used as the workspace for creating all the required files for the application.
(Note: If readers are downloading the code of the article mentioned in the link, then extract the code and open the same folder in the VSCode. The following steps can be implemented in the same folder.)
Step 2: In this folder, add a file of the name index.html. Right-click on this file and select the Open in Command Prompt option. This will open the command prompt. Run the following command from the command prompt to create package.json file.
npm init -y
The package.json will be created with the default settings. Modify this file by defining following dependencies in the dependencies section which are required for the application.
"@angular/common": "4.1.1",
"@angular/compiler": "4.1.1",
"@angular/core": "4.1.1",
"@angular/forms": "4.1.1",
"@angular/http": "4.1.1",
"@angular/platform-browser": "4.1.1",
"@angular/platform-browser-dynamic": "4.1.1",
"@angular/router": "4.1.1",
"angular-in-memory-web-api": "0.3.2",
"systemjs": "0.20.12",
"core-js": "2.4.1",
"rxjs": "5.3.1",
"zone.js": "0.8.10",
"bootstrap": "3.3.7",
"es6-shim": "0.35.2",
"koa": "2.2.0",
"koa-static": "3.0.0",
"livereload": "0.6.0",
"reflect-metadata": "0.1.9",
"@types/es6-shim": "0.31.33",
"body-parser": "1.17.2",
"cors": "2.8.3",
"express": "4.15.3",
"mongo": "0.1.0",
"mongoose": "4.10.5"
Add the following dependencies in the devDependencies section of the package.json file:
"@types/node": "^6.0.46",
"@types/jasmine": "2.5.36",
"concurrently": "3.1.0",
"lite-server": "2.2.2",
"node-gyp": "3.4.0",
"typescript": "2.3.2",
"typings": "2.1.1",
"@types/es6-shim": "0.31.33"
Save the package.json.
Run the following command from the command prompt.
npm install
This will install all dependencies for the project.
Step 3: In the workspace, add a new folder of name app. In this folder, add a new file of name employee.model.ts. In this file add the following code.
export class Employee {
constructor(
public _id: string,
public EmpNo: number,
public EmpName: string,
public Salary: number,
public DeptName: string,
public Designation: string) { }
}
This class will be used for performing CRUD operations.
Step 4: In the app folder, add a new file of the name app.service.ts. Add the following code in the file which will use the HTTP object to make call to the REST API.
//1. Import all dependencies
import { Injectable } from '@angular/core';
import {Http, Response, RequestOptions, Headers} from '@angular/http';
import {Employee} from './employee.model';
import {Observable} from 'rxjs/Observable';
import 'rxjs/Rx';
//2. The service class
@Injectable()
export class EmployeeService {
//3. The local private variable for storing the URL of the REST API
private servUrl = "http://localhost:8020/EmployeeList/api/employees";
//4. Passsing the Http dependency to the constructor to access Http functions
constructor(private http: Http) { }
//5. Function to return the Observable response containing all Employees
getEmployees(): Observable<Response> {
return this.http.get(this.servUrl);
}
//6. Function to perform POST operation to create a new employee
addEmployee(emp: Employee): Observable<Response> {
let header = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: header });
return this
.http
.post(this.servUrl, JSON.stringify(emp), options)
}
//7. Function to update Employee using PUT operation
updateEmployee(id: string, emp: Employee): Observable<Response> {
let header = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: header });
return this
.http
.put(this.servUrl + `/` + id, JSON.stringify(emp), options)
}
//8. Function to remove the Employee using DELETE operation
deleteEmployee(id: string): Observable<Response> {
return this
.http
.delete(this.servUrl + `/` + id)
}
}
The above code defines the EmployeeService class. This class is applied with the Injectable decorator. This decorator will use the class for dependency injection.
The above code contains following features. (The following numbering matches with the comments applied on the above code.)
1. Imports all dependencies for creating Service.
2. The EmployeeService class is applied with the Injectable decorator so that it can be used for dependency injection.
3. The servUrl variable is assigned with the URL of the REST API.
4. The constructor of the class accepts Http object. This provides functions for performing Http operations like GET/POST/PUT/DELETE using get()/post()/put()/delete() functions. All these functions return an Observable<Response> object.
5. The getEmployees() function makes Http GET request to the REST API to read all Employees.
6. The addEmployee() function makes Http POST request to REST API to create a new Employee.
7. The updateEmployee() function makes Http PUT request to REST API to update an Employee.
8. The deleteEmployee() function makes Http DELETE request to REST API to delete an Employee .
Step 5: In the app folder, add a new file of name app.component.ts. Add the following code in this file
import {TemplateRef, ViewChild} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {Employee} from './employee.model';
import {EmployeeService} from './app.service';
import {Headers, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/Rx';
@Component({ selector: 'app-data', templateUrl: './app/app.component.html' })
export class AppComponent implements OnInit {
//1. Template Ref types
@ViewChild('readOnlyTemplate') readOnlyTemplate: TemplateRef<any>;
@ViewChild('editTemplate') editTemplate: TemplateRef<any>;
//2. Other Variables
message: string;
employee: Employee;
selemp: Employee;
employees: Array<Employee>;
isNewRecord: boolean;
statusMessage: string;
//3. Constructor injected with the Service Dependency
constructor(private serv: EmployeeService) {
this.employees = new Array<Employee>();
this.message = 'HTML DataGrid using Angular 4';
}
//4. Load all Employees
ngOnInit() {
this.loadEmployee();
}
private loadEmployee() {
this
.serv
.getEmployees()
.subscribe((resp: Response) => {
this.employees = resp.json();
//console.log(JSON.stringify(resp.json()));
});
}
//5. Add Employee
addEmp() {
this.selemp = new Employee('', 0, '', 0, '', '');
this
.employees
.push(this.selemp);
this.isNewRecord = true;
//return this.editTemplate;
}
//6. Edit Employee
editEmployee(emp: Employee) {
this.selemp = emp;
}
//7. Load either Read-Onoy Template or EditTemplate
loadTemplate(emp: Employee) {
if (this.selemp && this.selemp.EmpNo == emp.EmpNo) {
return this.editTemplate;
} else {
return this.readOnlyTemplate;
}
}
//8. Save Employee
saveEmp() {
if (this.isNewRecord) {
//add a new Employee
this.serv.addEmployee(this.selemp).subscribe((resp: Response) => {
this.employee = resp.json(),
this.statusMessage = 'Record Added Successfully.',
this.loadEmployee();
});
this.isNewRecord = false;
this.selemp = null;
} else {
//edit the record
this.serv.updateEmployee(this.selemp._id, this.selemp).subscribe((resp: Response) => {
this.statusMessage = 'Record Updated Successfully.',
this.loadEmployee();
});
this.selemp = null;
}
}
//9. Cancel edit
cancel() {
this.selemp = null;
}
//10 Delete Employee
deleteEmp(emp: Employee) {
this.serv.deleteEmployee(emp._id).subscribe((resp: Response) => {
this.statusMessage = 'Record Deleted Successfully.',
this.loadEmployee();
});
}
}
The above code defines an Angular component, which contains the following specifications:
The component in the current example needs ViewChild object, this is used to configure a view query. This means that it gets the first element or the directive matching the selector from the view DOM.
In this example, the HTML table will be used as a DataGrid for performing CRUD operations. To display Editable and Read-only table rows, the matching DOM templates must be toggled based on events.
The ViewChild object will be useful to configure query on the view to select the matching DOM to update or toggle from editable template to read-only template.
The TemplateRef object is used to instantiate Embedded views so that they can be used as ViewChild for toggling.
The Component class is defined using @Component directive with its selector and templateUrl properties. The templateUrl property accepts the html file which will be rendered when the selector is processed in the browser.
The following line numbers matches with the comments applied on the above code:
1. This declares the TemplateRef objects using ViewChild directive. The readOnlyTemplate and editTemplate will be declared using ng-template in the html file in future steps.
2. This declare other variables used for the application execution.
3. The constructor is injected with the service object declared in previous step. The constructor initializes employees array and message property. These will be used for databinding on View.
4. The ngOnInit() function calls loadEmployee() function. This function calls the Angular service and subscribes with the response received from it. This response contains Employees returned from the Angular service.
5. The addEmp() function pushes an empty Employee object in the Employees array and sets the IsNewRecord flag to true so that the empty row can be generated in the Html table which is showing Employees array.
6. The editEmployee() function accepts an Employee object which will be updated.
7. The loadTemplate() function accept Employee object and based on the condition it toggles between editTemplate and read-only templates.
8. The saveEmp() function either calls the addEmployee() function or updateEmployee() function of the service based on the flag for performing add new employee operation or update employee operation .
9. The cancel() function sets the selEmp object to null to cancel Edit effect on the html table.
10. The deleteEmp() function calls the deleteEmployee() function of the service to delete the Employee based on Id.
Step 6: In the app folder, add a new file of name app.component.html. Add the following markup in this file:
<h1>{{message}}</h1>
<hr>
<input type="button" value="Add" class="btn btn-default" (click)="addEmp()" />
<div style="overflow:auto">
<table class="table table-bordered table-striped">
<thead>
<tr>
<td>Id</td>
<td>EmpNo</td>
<td>EmpName</td>
<td>Salary</td>
<td>DeptName</td>
<td>Designation</td>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
<tr *ngFor="let emp of employees;let i=idx">
<ng-template [ngTemplateOutlet]="loadTemplate(emp)" [ngOutletContext]="{ $implicit: emp, idx: i }"></ng-template>
</tr>
</tbody>
</table>
</div>
<div>{{statusMessage}}</div>
<!--The Html Template for Read-Only Rows-->
<ng-template #readOnlyTemplate let-emp>
<td>{{emp._id}}</td>
<td>{{emp.EmpNo}}</td>
<td>{{emp.EmpName}}</td>
<td>{{emp.Salary}}</td>
<td>{{emp.DeptName}}</td>
<td>{{emp.Designation}}</td>
<td>
<input type="button" value="Edit" class="btn btn-default" (click)="editEmployee(emp)" />
</td>
<td>
<input type="button" value="Delete" (click)="deleteEmp(emp)" class="btn btn-danger" />
</td>
</ng-template>
<!--Ends Here-->
<!--The Html Template for Editable Rows-->
<ng-template #editTemplate>
<td>
<input type="text" [(ngModel)]="selemp._id" readonly disabled />
</td>
<td>
<input type="text" [(ngModel)]="selemp.EmpNo" />
</td>
<td>
<input type="text" [(ngModel)]="selemp.EmpName" />
</td>
<td>
<input type="text" [(ngModel)]="selemp.Salary" />
</td>
<td>
<input type="text" [(ngModel)]="selemp.DeptName" />
</td>
<td>
<input type="text" [(ngModel)]="selemp.Designation" />
</td>
<td>
<input type="button" value="Save" (click)="saveEmp()" class="btn btn-success" />
</td>
<td>
<input type="button" value="Cancel" (click)="cancel()" class="btn btn-warning" />
</td>
</ng-template>
<!--Ends Here-->
The above markup defines Angular templates using ng-template of name readOnlyTemplate and editTemplate. These templates will be loaded in table body using *ngFor iteration which iterates through employees defined in the Component class defined in the previous step.
This iteration loads templates using ng-template tag.
This object has the following directive and property:
- ngTemplateOutlet Directive - used to insert an embedded view from a TemplateRef. In the markup, this property is bound with the loadTemplate() function defined in the component class. This function will either load editTemplate or readOnlyTemplate based on the state of the condition defined in the code.
- ngOutletContext Property - used by the ngTemplateOutlet property to embed the template in the DOM. The $implicit object is used to set the default value for the template. In the current case, the default value is emp object from the employees’ array.
The readOnlyTemplate is used to display the Employee record in a read-only mode. This contains Edit and Delete buttons to either edit or delete Employee record. The editTemplate defines all textboxes bound with properties of the Employee class. This contains Save and Cancel buttons to perform save (addnew and update) and cancel operations.
Step 7: In the app folder, add new file of name main.ts. Add the following code in it:
import { NgModule } from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component';
import {EmployeeService} from './app.service';
@NgModule({
imports: [BrowserModule, FormsModule, HttpModule],
declarations: [AppComponent],
providers: [EmployeeService],
bootstrap: [AppComponent]
})
class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
This is an Angular bootstrap module which imports necessary modules dependencies for the application. Using @NgModule decorator, the AppComponent class defined in Step 5 is declared and set as bootstrap and EmployeeService defined in Step 4, is registered as a provider so that it can be used for dependency injection in Component class.
Step 8: In the root of the workspace, add a new file of the name systemjs.config.js. This file will use System object which is used for module loading of standard Angular modules and other modules for the application.
Add the following code in it.
var map = {
"rxjs": "node_modules/rxjs",
"@angular/common": "node_modules/@angular/common",
"@angular/compiler": "node_modules/@angular/compiler",
"@angular/core": "node_modules/@angular/core",
"@angular/core/@angular/core": "node_modules/@angular/core/@angular/core",
"@angular/forms": "node_modules/@angular/forms",
"@angular/platform-browser": "node_modules/@angular/platform-browser",
"@angular/platform-browser-dynamic": "node_modules/@angular/platform-browser-dynamic",
"@angular/http": "node_modules/@angular/http"
};
var packages = {
"rxjs": { "defaultExtension": "js" },
"@angular/common": { "main": "bundles/common.umd.js", "defaultExtension": "js" },
"@angular/compiler": { "main": "bundles/compiler.umd.js", "defaultExtension": "js" },
"@angular/core": { "main": "bundles/core.umd.js", "defaultExtension": "js" },
"@angular/core/@angular/core": { "main": "core.js", "defaultExtension": "js" },
"@angular/forms": { "main": "bundles/forms.umd.js", "defaultExtension": "js" },
"@angular/platform-browser": { "main": "bundles/platform-browser.umd.js", "defaultExtension": "js" },
"@angular/http": { "main": "bundles/http.umd.js", "defaultExtension": "js" },
"@angular/platform-browser-dynamic": { "main": "bundles/platform-browser-dynamic.umd.js", "defaultExtension": "js" },
"app": {
format: 'register',
defaultExtension: 'js'
}
};
var config = {
map: map,
packages: packages
};
System.config(config);
Step 9: Add the following code in index.html:
<!DOCTYPE html>
<html>
<head>
<title>Angular 4 App</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css"></link>
</head>
<body>
<h1>Angular 2 Application</h1>
<app-data></app-data>
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('./app/main')
.then(null, console.error.bind(console));
</script>
</body>
</html>
The above markup refers to the required scripts and systemjs.config.js to load all modules in the browser. The System object and its import() function is used to load main file defined in Step 7 for bootstrapping.
Step 10: Add a new file in the Workspace of name appserver.js with the following code in it. This code uses KOA module to serve Angular application to the client
var koa = require("koa");
var serve = require("koa-static");
var livereload = require("livereload");
var app = new koa();
var server = livereload.createServer();
server.watch(__dirname + "/app/*.js");
app.use(serve("."));
app.listen(9001);
The Angular application is hosted on port 9001.
Running the Angular Application
Add the following code in package.json to run the KOA server to deliver Angular application to the browser
"scripts": {
"start": "concurrently \"tsc -w\" \"node appserver.js\" \"node apiserver.js\"",
"tsc": "tsc",
"tsc:w": "tsc -w",
"postinstall": "typings install"
}
This will compile TypeScript files (.ts) into JavaScript files and run REST API (apiserver.js) and Angular (appserver.js) using a single command. As mentioned in the link, start the MongoDB server. Enter the following command on the command prompt:
npm run start
Open the browser and enter the following URL in the address base:
http://localhost:9001
This will show the following view:
Click on the Add button, the table will be added with the empty editable row as shown in the following image:
Enter data in textboxes and click on Save button, the record will be saved and editable row will be replaced by read-only row as shown in the following image:
If the Edit button is clicked, the read-only row will be replaced with editable row with data in it as shown in the following image:
Make changes in record e.g. change EmpName from Mahesh to Mahesh Sabnis and click on the Save button, the data will be saved as shown in the following image:
Likewise delete functionality can also be tested.
Conclusion
In Angular, using the ng-template, a WebComponent can be defined which can be dynamically embedded in the DOM using @ViewChild and TemplateRef objects. Hence it is easily possible to define complex UI like Data Grid with databinding in Angular.
Download the entire source code of this article (Github)
This article was technically reviewed by Ravi Kiran.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Mahesh Sabnis is a DotNetCurry author and a Microsoft MVP having over two decades of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions), and Front-end technologies like Angular and React. Follow him on twitter @
maheshdotnet or connect with him on
LinkedIn