Introduction
This article is Part 2 and a follow up to the previous article, Developing And Deploying An Angular 2 Application with Visual Studio 2015 which discussed how to configure Visual Studio Professional 2015 for Angular 2 development and production deployment.
The previous article also stepped through using Gulp to package an Angular 2 application for both development and production release purposes including the bundling and injecting of HTML templates into Angular 2 components. This article will dig into some of the key aspects of the Angular 2 and TypeScript code for the sample customer maintenance application included in both articles. This article will focus on Release Candidate 4 of Angular 2. Release Candidate 5 was just released so the final release of Angular 2 should be out before the end of the year. I'll revisit and update this article when the final release is available.
Overview and Goals
For the purposes of learning Angular 2 (Release Candidate 4), the sample web application for this article was developed with Visual Studio Professional 2015 and will consist of the following functionality and goals:
- Allow the user to register, login and update their user profile
- Allow the user to create, update and browse a customer database
- Create an Angular 2 front-end and a Microsoft .NET backend
- Develop the Angular 2 application in TypeScript
- Develop some custom homegrown controls and widgets where needed
For the purposes of this demo, the Microsoft Web API architecture and plumbing was included with the project. The project solution will integrate both the front-end and back-end code with the Web API accepting and responding to RESTful web service requests utilizing IIS Express which is integrated with Visual Studio 2015.
The sample application for this article will use Microsoft ASP.NET 4. A new version of .ASP.NET has been released called ASP.NET Core. Formerly known as ASP.NET 5, ASP.NET Core is a significant redesign of ASP.NET and has a number of architectural changes that result in a much leaner and modular framework. ASP.NET Core is no longer based on System.Web.dll. I believe this is Microsoft's response to the proliferation of Node.js and it's lightweight footprint and open architecture. Competition is good. Perhaps in a future article, I'll integrate Angular 2 with ASP.NET Core, in the meantime, there is a lot to learn.
Installation and the Running the Sample Application
To run the sample application after downloading and unzipping the attached source code, you'll need to run "npm install
" from the command line in the root folder of the project. This assumes you already have NodeJs installed on your computer. The node_modules folder is large so I only included the minimum node modules to compile the application. After compiling and launching the application, you can login with a username and password as follows:
UserName: bgates@microsoft.com
Password: microsoft
Optionally, you can register your own login and password.
TypeScript
The entire customer maintenance sample application was written in TypeScript. Alternatively, you can write Angular 2 applications in pure JavaScript, but I chose TypeScript for various reasons. TypeScript enhances JavaScript with types, classes and interfaces that mimics strongly-typed languages. If you are a C# developer or a Java developer, you will love the look and feel of the syntax as it closely resembles the syntax of these strongly typed languages.
With a good IDE like Visual Studio, you get great IntelliSense
and code completion which enhances the development experience. Additionally, when you compile and build your application in Visual Studio, all the TypeScript code gets compiled too. If there are any syntax and/or type errors in your code, the Visual Studio build will fail. All this is a great improvement for client-side web development that we all are used to getting for server-side development.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Address } from '../entities/address.entity';
import { Customer } from '../entities/customer.entity';
import { AlertBoxComponent } from '../shared/alertbox.component';
import { CustomerService } from '../services/customer.service';
import { HttpService } from '../services/http.service';
import { AlertService } from '../services/alert.service';
import { SessionService } from '../services/session.service';
import { AddressComponent } from '../shared/address.component';
@Component({
templateUrl: 'application/customer/customer-maintenance.component.html',
providers: [AlertService],
directives: [AlertBoxComponent, AddressComponent]
})
export class CustomerMaintenanceComponent implements OnInit {
public title: string = 'Customer Maintenance';
public customerID: number;
public customerCode: string;
public companyName: string;
public phoneNumber: string;
public address: Address;
public showUpdateButton: Boolean;
public showAddButton: Boolean;
public customerCodeInputError: Boolean;
public companyNameInputError: Boolean;
public messageBox: string;
public alerts: Array<string =[];
constructor(private route ActivatedRoute,
private customerService CustomerService,
private sessionService SessionService,
private alertService AlertService) {
}
private ngOnInit() {
this.showUpdateButton=false;
this.showAddButton=false;
this.address=new Address();
this.route.params.subscribe(params=> {
let id: string = params['id'];
if (id != undefined) {
this.customerID = parseInt(id);
let customer = new Customer();
customer.customerID = this.customerID;
this.customerService.getCustomer(customer)
.subscribe(
response => this.getCustomerOnSuccess(response),
response => this.getCustomerOnError(response));
}
else {
this.customerID = 0;
this.showAddButton = true;
this.showUpdateButton = false;
}
});
}
private getCustomerOnSuccess(response: Customer) {
this.customerCode = response.customerCode;
this.companyName = response.companyName;
this.phoneNumber = response.phoneNumber;
this.address.addressLine1 = response.addressLine1;
this.address.addressLine2 = response.addressLine2;
this.address.city = response.city;
this.address.state = response.state;
this.address.zipCode = response.zipCode;
this.showUpdateButton = true;
this.showAddButton = false;
}
private getCustomerOnError(response: Customer) {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
public updateCustomer(): void {
let customer = new Customer();
customer.customerID = this.customerID;
customer.customerCode = this.customerCode;
customer.companyName = this.companyName;
customer.phoneNumber = this.phoneNumber;
customer.addressLine1 = this.address.addressLine1;
customer.addressLine2 = this.address.addressLine2;
customer.city = this.address.city;
customer.state = this.address.state;
customer.zipCode = this.address.zipCode;
this.clearInputErrors();
this.customerService.updateCustomer(customer).subscribe(
response => this.updateCustomerOnSuccess(response),
response => this.updateCustomerOnError(response));
}
private updateCustomerOnSuccess(response: Customer) {
if (this.customerID == 0) {
this.customerID = response.customerID;
this.showAddButton = false;
this.showUpdateButton = true;
}
this.alertService.renderSuccessMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
private updateCustomerOnError(response: Customer) {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
private clearInputErrors() {
this.customerCodeInputError = false;
this.companyNameInputError = false;
}
}
Most of the TypeScript code in the sample code above can be considered added sugar. For example, you can decorate your properties and methods as being private
or public
and give them strong types. But when the code gets compiled down to JavaScript, these TypeScript conventions are removed from the resulting generated JavaScript. It mainly provides for a better development experience and tighter coding practices. For the purposes of the sample application as a coding standard, I made all the component methods public
that are accessed by the HTML template and all the methods called internally by the component private
.
Start Up and Injection
The first thing that you need to do to get your application up and running is to bootstrap
the application.
Bootstrapping your application in Angular 2 is quite a bit different than in Angular 1 where we added “ng-app
” to the HTML body tag to start the application. Bootstrapping in Angular 2 uses the ‘bootstrap
’ function.
In the sample main.ts code below, the Angular 2 bootstrap method takes in your initial root application component as a parameter - in this case ApplicationComponent
- and a second parameter for any additional providers that your application needs. Components in Angular 2 are basically classes.
The reference to typings/browser.d.ts allows Visual Studio to compile and understand TypeScript typings and provides for the proper IntelliSense.
import { bootstrap } from '@angular/platform-browser-dynamic';
import { HTTP_BINDINGS } from '@angular/http';
import { ApplicationComponent } from './application.component';
import { applicationRouterProviders } from './application-routes';
import { enableProdMode } from '@angular/core';
enableProdMode();
bootstrap(ApplicationComponent, [applicationRouterProviders]);
During development, the main bootstrap code is launched in the default page through SystemJS
. When deploying to production, the entire application is bundled into a CommonJS
format where a micro-loader
is added to the end of the bundled JavaScript. This is described in detail in my previous Angular 2 article.
<!--
<script src="~/systemjs.config.js"></script>
<script>
System.import('application/main.js').catch(function (err) { console.error(err); });
</script>
<codeproject-application title="@title"
currentRoute="@currentRoute" version="@version">
<div>
...loading
</div>
</codeproject-application>
The root ApplicationComponent
references the master page component that provides for the structure and layout of the application, including a header section, menu bar, the content section and a footer.
The sample application uses configuration settings from the web.config file. In the sample code below the application version, title and the current route values are injected into the Angular 2 application and referenced in the constructor. These values are injected into the master page through input parameters of the master page.
import { Component, ElementRef, ApplicationRef } from '@angular/core';
import { MasterComponent } from './shared/master.component';
import 'rxjs/Rx';
@Component({
selector: 'codeproject-application',
template: '<master [currentRoute]="currentRoute"
[title]="title"
[version]="version">
</master>',
directives: [MasterComponent]
})
export class ApplicationComponent {
public title: string;
public currentRoute: string;
public version: string;
constructor(private elementRef: ElementRef) {
let native = this.elementRef.nativeElement;
this.title = native.getAttribute("title");
this.currentRoute = native.getAttribute("currentRoute");
this.version = native.getAttribute("version");
}
}
Injecting values into Angular 2 comes in especially handy when you need to tell an Angular 2 application what the server URL is for all the RESTFul web service calls that the application needs to make.
Application Routes
Every Angular 2 application uses routing to navigate from page to page. In the sample application, a separate TypeScript file was created that references each route. Each route is configured and tied to an Angular 2 component.
import { provideRouter, RouterConfig } from "@angular/router";
import { AboutComponent } from './home/about.component';
import { RegisterComponent } from './home/register.component';
import { LoginComponent } from './home/login.component';
import { ContactComponent } from './home/contact.component';
import { MasterComponent } from './shared/master.component';
import { HomeComponent } from './home/home.component';
import { ImportCustomersComponent } from './home/import-customers.component';
import { CustomerInquiryComponent } from './customer/customer-inquiry.component';
import { CustomerMaintenanceComponent } from './customer/customer-maintenance.component';
import { UserProfileComponent } from './user/user-profile.component';
import { SessionService } from './services/session.service';
import { authorizationProviders } from "./authorization-providers";
import { AuthorizationGuard } from "./authorization-guard";
const routes: RouterConfig = [
{ path: '', component: HomeComponent },
{ path: 'home/about', component: AboutComponent },
{ path: 'home/contact', component: ContactComponent },
{ path: 'home/home', component: HomeComponent },
{ path: 'home/register', component: RegisterComponent },
{ path: 'home/login', component: LoginComponent },
{ path: 'home/importcustomers', component: ImportCustomersComponent },
{ path: 'customer/customerinquiry', component: CustomerInquiryComponent,
canActivate: [AuthorizationGuard] },
{ path: 'customer/customermaintenance', component: CustomerMaintenanceComponent,
canActivate: [AuthorizationGuard] },
{ path: 'customer/customermaintenance/:id', component: CustomerMaintenanceComponent,
canActivate: [AuthorizationGuard] },
{ path: 'user/profile', component: UserProfileComponent,
canActivate: [AuthorizationGuard] }
];
export const applicationRouterProviders = [
provideRouter(routes),
authorizationProviders
];
Protecting Routes
One of the things you might want to do in an Angular 2 application is protect certain routes from being accessed if the user hasn't been authenticated. The application routes code above makes reference to a property called canActivate
. This property references a class called AuthorizationGuard
. When a protected route is accessed, the AuthorizationGuard
component is executed and checks to see if the user has been authenticated. If the user has not been authenticated, the canActivate
method routes the user to the default route which will end up sending the user to the login page.
import { Injectable, Component } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { SessionService } from "./services/session.service";
import { User } from "./entities/user.entity";
@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(private _router: Router, private sessionService: SessionService) { }
public canActivate() {
if (this.sessionService.isAuthenicated==true) {
return true;
}
this._router.navigate(['/']);
return false;
}
}
Session State
For the sample application, there was a need to maintain the user's session information throughout the lifetime of the user's logged in session. Most web applications these days are completely stateless while using a server side architecture that incorporates RESTful API services. To maintain the user's session information, such as name and email address information, an Angular 2 service was needed. In Angular 1, we could handle this with either a factory or a service. In Angular 2, things are simplified for us. We only need to create a component class that acts as a service that contains the properties and methods that the application needs.
import { Injectable, EventEmitter } from '@angular/core';
import { User } from '../entities/user.entity';
@Injectable()
export class SessionService {
public firstName: string;
public lastName: string;
public emailAddress: string;
public addressLine1: string;
public addressLine2: string;
public city: string;
public state: string;
public zipCode: string;
public userID: number;
public isAuthenicated: Boolean;
public sessionEvent: EventEmitter<any>;
public apiServer: string;
public version: string;
constructor() {
this.sessionEvent = new EventEmitter();
}
public authenticated(user: User) {
this.userID = user.userID;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.emailAddress = user.emailAddress;
this.addressLine1 = user.addressLine1;
this.addressLine2 = user.addressLine2;
this.city = user.city;
this.state = user.state;
this.zipCode = user.zipCode;
this.isAuthenicated = true;
this.sessionEvent.emit(user);
}
public logout() {
this.userID = 0;
this.firstName = "";
this.lastName = "";
this.emailAddress = "";
this.addressLine1 = "";
this.addressLine2 = "";
this.city = "";
this.state = "";
this.zipCode = "";
this.isAuthenicated = false;
}
}
Providers and Singletons
One common pattern in OOP is the Singleton Pattern which allows us to have one instance of a class object in our entire application. To create an Angular 2 service that can maintain a user's session information in a stateful way requires the service to be provided for and created as a singleton service component.
Since the SessionService
class needs to be accessed throughout the application, the SessionService
is provided for at the top of the application tree in the MasterComponent
.
Providers are usually singleton (one instance) objects, that other objects can access through dependency injection (DI). If you plan to use an object multiple times, like the SessionService
in this application in different components, you can ask for the same instance of that service and reuse it. You do that with the help of DI by providing a reference to the same object that DI creates for you.
In the sample code snippet from the MasterComponent
, the provider property of the @Component
annotation creates an instance of all the objects in the list between the brackets.
Care should be taken not to create another component that references a singleton object through a provider. A singleton only needs to be provider for once at the top of the application tree. If you provide for the same class again, a new instance of the object is created and you will lose reference to any needed information.
@Component({
selector: 'master',
templateUrl: 'application/shared/master.component.html',
directives: [ROUTER_DIRECTIVES],
providers: [HTTP_PROVIDERS, UserService, CustomerService, HttpService, BlockUIService]
})
Dependency Injection has always been one of Angular’s biggest features and selling points. It allows us to inject dependencies in different components across our applications,
In Angular 2, Dependency Injection is performed through the constructor of a component's class. Any services or components needed by a component are injected into the component by adding the dependency in the component's constructor method. Dependency Injection makes our code more testable and testable code makes our code more reusable and vise versa.
constructor(private customerService: CustomerService,
private sessionService: SessionService ) { }
Remember, Angular 2 works as a tree structure. An application will always have a root component that contains all other components. Child components can also contain providers that list components that its children may also inject. Children of children components have access to all the injected components that are provided for by their parent component as singleton components.
Block UI
One of the challenges with using pre-release versions of completely rewritten frameworks like Angular 2 is that not all the functionality that you are used to in the previous version are available yet for the new version. One of the things I wanted to include in the sample application was some UI blocking
functionality that I used in Angular 1.
The UI Blocking functionality in my Angular 1 applications would create a light filter over the entire page to block the UI from user interaction and would show a "please wait" message while an HTTP RESTful service call is being executed. Unfortunately, the Block UI I used in Angular 1 was not available for Angular 2 when I was writing the code for this article.
After a little research, I figured out a way to create a custom block UI solution. To start, I added a little HTML and some CSS to the index default page that acted as a container DIV
for a full page blocking filter with a loading spinning circle and a "please wait" message displayed. This HTML is added to the DOM when a blockUI
property is set to true
in the master page component.
<!--
<div *ngIf="blockUI">
<div class="in modal-backdrop spinner-overlay"></div>
<div class="spinner-message-container"
aria-live="assertive" aria-atomic="true">
<div class="loading-message">
<img src="application/content/images/loading-spinner-grey.gif"
class="rotate90">
<span> please wait...</span>
</div>
</div>
</div>
To allow the sample application to toggle the custom Block
UI functionality from anywhere in the application, a singleton BlockUI
service needed to be created.
The Block
UI service uses an Angular 2 Event Emitter. Angular 2 components can emit custom events through the new EventEmitter
component. Angular 2 event emitters allow you to broadcast events (emit) and pass data to those components that are subscribing to the event which lets the subscriber listen for raised events and take action.
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class BlockUIService {
public blockUIEvent: EventEmitter<any>;
constructor() {
this.blockUIEvent = new EventEmitter();
}
public startBlock() {
this.blockUIEvent.emit(true);
}
public stopBlock() {
this.blockUIEvent.emit(false);
}
}
The Block
UI service starts a new Event Emitter and raises the event and emits a value of true
when the startBlock
method is executed and a value of false
is emitted when the stopBlock
is executed.
constructor(
private sessionService: SessionService,
private applicationRef: ApplicationRef,
private userService: UserService,
private blockUIService: BlockUIService,
private router: Router) {
router.events.subscribe((uri) => {
applicationRef.zone.run(() => applicationRef.tick());
});
}
public ngOnInit() {
this.sessionService.sessionEvent.subscribe(user => this.onAuthenication(user));
this.blockUIService.blockUIEvent.subscribe(event => this.blockUnBlockUI(event));
this.blockUIService.blockUIEvent.emit({
value: true
});
let user: User = new User();
this.userService.authenicate(user).subscribe(
response => this.authenicateOnSuccess(response),
response => this.authenicateOnError(response));
}
private blockUnBlockUI(event) {
this.blockUI = event.value;
}
private authenicateOnSuccess(response: User) {
this.blockUIService.blockUIEvent.emit({
value: false
});
}
In the master page component above, the Angular 2 OnInit
event is referenced and executed on component start-up. When the master page component starts-up, it subscribes to the blockUI
event emitter. When subscribing to an event emitter, you specify which method to call when the event is raised. In the sample above, the blockUnblockUI
method is called when block UI events are raised and the value of true
or false
is set to the blockUI
property which displays or removes the blocking UI filter and "please wait" message.
SIDE NOTE: In the above code snippet, you'll see a reference to a piece of code that executes the command.
applicationRef.zone.run(() => applicationRef.tick());
This was needed as a patch to fix a bug when hitting the back button in Internet Explorer. The back button would not go back to the previous route. Hopefully, this will be fixed in the final release.
HTTP Service
Another useful singleton service for the sample application for this article includes a HttpService
component for making HTTP RESTful service calls. Instead of calling the Angular 2 Http component directly throughout the application, I wanted to centralize the functionality so the HTTP call can be customized and easily reused.
The Angular2 http.post
method requires a url and a body, both strings and then optionally an options
object. In the custom HttpService
below, the headers
property is modified specifying that the request is a json request.
Additionally, the sample application uses a JSON web token to store authentication information that is also added to the header property. The JSON web token which was generated server-side is stored in the browser's local storage so it can be persisted and retrieved for subsequent requests.
The http.post
also returns an Observable. An Angular 2 Observable provides a new pattern for running asynchronous requests. The Angular 2 HTTP component by default returns an Observable
opposed to a Promise.
Observables provide for more functionality and flexibility over a Promise. By using Observables, we improve readability and maintenance of our code because Observables can respond gracefully to more complex scenarios involving multiple emitted values opposed to only a one-off single values and with the added ability to cancel an observable request.
In the sample code snippet below, the map function is used to extract the JSON object from the response when subscribing to the observable. With each call to the HttpPost
of this custom service, the blockUI
service is called that triggers the blockUI
filter.
When the response is returned, the json body is extracted and returned to calling function and the UI Blocking filter is removed from the UI.
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { SessionService } from '../services/session.service';
import { BlockUIService } from './blockui.service';
@Injectable()
export class HttpService {
constructor(private http: Http, private blockUIService: BlockUIService) {}
public httpPost(object: any, url: string): Observable<any> {
this.blockUIService.blockUIEvent.emit({
value: true
});
let body = JSON.stringify(object);
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'q=0.8;application/json;q=0.9');
if (typeof (Storage) !== "undefined") {
let token = localStorage.getItem("CodeProjectAngular2Token");
headers.append('Authorization', token);
}
let options = new RequestOptions({ headers: headers });
return this.http.post(url, body, options).map((response) =>
this.parseResponse(response, this.blockUIService, true))
.catch((err) => this.handleError(err, this.blockUIService, true));
}
public httpPostWithNoBlock(object: any, url: string): Observable<any> {
let body = JSON.stringify(object);
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'q=0.8;application/json;q=0.9');
if (typeof (Storage) !== "undefined") {
let token = localStorage.getItem("CodeProjectAngular2Token");
headers.append('Authorization', token);
}
let options = new RequestOptions({ headers: headers });
return this.http.post(url, body, options).map((response) =>
this.parseResponse(response, this.blockUIService, false))
.catch((err) => this.handleError(err, this.blockUIService, false));
}
private handleError(error: any, blockUIService: BlockUIService, blocking: Boolean) {
let body = error.json();
if (blocking) {
blockUIService.blockUIEvent.emit({
value: false
});
}
return Observable.throw(body);
}
private parseResponse(response: Response,
blockUIService: BlockUIService, blocking: Boolean) {
let authorizationToken = response.headers.get("Authorization");
if (authorizationToken != null) {
if (typeof (Storage) !== "undefined") {
localStorage.setItem("CodeProjectAngular2Token", authorizationToken);
}
}
if (blocking) {
blockUIService.blockUIEvent.emit({
value: false
});
}
let body = response.json();
return body;
}
}
Another tidbit to notice in the code snippet above is the line of code that appends to the header of the http request an 'Accept', 'q=0.8;application/json;q=0.9'
header item. This fixes a problem when parsing the json response when running the application in Firefox. In Firefox, JSON parsing fails without the added header in the request.
Consuming the HTTP Service
Another thing I wanted to do was keep all the http calls and their associated web API endpoint urls in separate services. In the example below, I created a CustomerService
that calls the HttpService
and passes in the url route for particular requests. This allows the url routes to be hidden from the higher-level page components so that RESTful service calls can be executed more generically. This is more of a preference than anything else and provides for an extra layer of abstraction. All customer related functionality that needs customer data will reference and execute methods in the CustomerService
to get data from the server.
import { Customer } from '../entities/customer.entity';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { HttpService } from './http.service';
@Injectable()
export class CustomerService {
constructor(private httpService: HttpService) { }
public importCustomers(customer: Customer): Observable<any> {
let url = "api/customers/importcustomers";
return this.httpService.httpPost(customer, url);
}
public getCustomers(customer: Customer): Observable<any> {
let url = "api/customers/getcustomers";
return this.httpService.httpPost(customer, url);
}
public getCustomer(customer: Customer): Observable<any> {
let url = "api/customers/getcustomer";
return this.httpService.httpPost(customer, url);
}
public updateCustomer(customer: Customer): Observable<any> {
let url = "api/customers/updatecustomer";
return this.httpService.httpPost(customer, url);
}
}
Custom Data Grid
Every web application needs some data grid functionality. At the time of writing of this article, there wasn't really any great data grid functionality for Angular 2. For the purposes of learning more about Angular 2, I decided to create my own data grid - with support for paging, sorting and the filtering and selecting of rows.
Most data grids you saw in Angular 1 required you to create a collection of columns that contained information such as column names. column headers and widths, etc., for formatting and displaying grid data in the controller. This is the approach I took for the components in the sample application that needed data grid functionality.
In the HTML Template for the data grid component below, contains three sections. The first section will loop through a collection of column information and display column headers with support to display up and down arrows when sorting the grid by a particular column. The sorting functionality is optionally enabled when specifying the columns you want to display in the data grid.
The second section of the HTML template loops through the collection of data rows that are data bound to the grid. Additional formatting of data is also being provided for in the grid. The *ngIf
directive makes it easy to display various different formats for displaying cell data or for including clickable row buttons in a cell.
The final section of the HTML template provides for the paging of data with the traditional first, previous, next and last buttons with additional information about the total number of rows and pages to be displayed.
<!--
<table class="table table-striped">
<thead>
<tr>
<td *ngFor="let col of columns"
[ngStyle]="{'width': col.cellWidth, 'text-align': col.textAlign}">
<div *ngIf="col.disableSorting==false">
<b><a (click)="sortData(col.name)">{{col.description}}</a></b>
<span *ngIf="col.name == sortColumn && sortAscending == true">
<i class="glyphicon glyphicon-arrow-down"></i>
</span>
<span *ngIf="col.name == sortColumn && sortDesending == true">
<i class="glyphicon glyphicon-arrow-up"></i>
</span>
</div>
<div *ngIf="col.disableSorting==true">
<b>{{col.description}}</b>
</div>
</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows; let i = index">
<td *ngFor="let col of columns"
[ngStyle]="{'width': col.width, 'text-align': col.textAlign}">
<div *ngIf="col.hyperLink == false &&
col.singleButton == false && col.multiButton == false">
<span class="table-responsive-custom"><b>
{{col.description}}: </b></span>
<div *ngIf="col.formatDate == true && col.formatDateTime == false">
{{row[col.name] | date:"MM/dd/yyyy"}}
</div>
<div *ngIf="col.formatDateTime == true && col.formatDate == false">
{{row[col.name] | date:"MM/dd/yyyy hh:mm AMPM" }}
</div>
<div *ngIf="col.formatDate == false && col.formatDateTime == false">
{{row[col.name]}}
</div>
</div>
<div *ngIf="col.hyperLink == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<div style="text-decoration: underline; cursor:pointer;"
(click)="selectedRow(i)">
<div *ngIf="col.formatDate == true && col.formatDateTime == false">
{{row[col.name] | date:"MM/dd/yyyy"}}
</div>
<div *ngIf="col.formatDateTime == true && col.formatDate == false">
{{row[col.name] | date:"MM/dd/yyy hh:mm AMPM" }}
</div>
<div *ngIf="col.formatDate == false && col.formatDateTime == false">
{{row[col.name]}}
</div>
</div>
</div>
<div *ngIf="col.singleButton == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<button class="btn btn-primary" (click)="buttonClicked(col.buttonText,i)">
<b>{{col.buttonText}}</b>
</button>
</div>
<div *ngIf="col.multiButton == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<div *ngFor="let button of col.buttons" style="float:left">
<button class="btn btn-primary"
(click)="buttonClicked(button.ButtonText,i)">
<b>{{button.ButtonText}} </b>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div style="float:left">
<button class="btn ui-grid-pager-control" (click)="buttonFirstPage()"
[disabled]="disableFirstPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="first-triangle"><div class="first-bar"></div></div>
</button>
<button class="btn ui-grid-pager-control" (click)="buttonPreviousPage()"
[disabled]="disablePreviousPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="first-triangle prev-triangle"></div>
</button>
{{currentPageNumber}} / {{totalPages}}
<button class="btn ui-grid-pager-control" (click)="buttonNextPage()"
[disabled]="disableNextPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="last-triangle"></div>
</button>
<button class="btn ui-grid-pager-control" (click)="buttonLastPage()"
[disabled]="disableLastPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="last-triangle"><div class="last-bar"></div></div>
</button>
<select class="ui-grid-pager-row-count-picker" [(ngModel)]="pageSize"
(change)="pageSizeChanged($event.target.value)">
<option *ngFor="let pageSizeDefault of pageSizes"
value="{{pageSizeDefault}}">
{{pageSizeDefault}}
</option>
</select>
items per page
</div>
<!--
<div class="grid-pager-responsive-custom">
{{itemNumberBegin}} -
{{itemNumberEnd}} of {{totalRows}}
items
</div>
Data Grid Component
The DataGrid
component takes two input parameters, one for a collection of columns and a collection of rows to be displayed in the grid. The DataGrid
component also uses an Event Emitter that fires off paging and sorting events. Additional events are also supported for row selection and for button click events for any buttons configured for any of the cell rows.
To make the data grid consumable, a selector property is set to allow the data grid to be included in a consuming HTML template. The data grid also contains its own CSS class file. Allowing individual CSS files at the component level is another improvement in Angular 2.
import { Component, EventEmitter, Injectable, Output, Input, OnChanges, OnInit, Host}
from '@angular/core';
import { DataGridColumn, DataGridSorter, DataGridButton, DataGridSortInformation,
DataGridEventInformation } from './datagrid.core';
import { TransactionalInformation } from '../../entities/transactionalinformation.entity';
@Component({
selector: 'datagrid',
styleUrls: ['application/shared/datagrid/datagrid.css'],
inputs: ['rows: rows', 'columns: columns'],
templateUrl: 'application/shared/datagrid/datagrid.component.html'
})
@Injectable()
export class DataGrid implements OnInit {
public columns: Array<DataGridColumn>;
public rows: Array<any>;
public sorter: DataGridSorter;
public pageSizes = [];
public sortColumn: string;
public sortDesending: Boolean;
public sortAscending: Boolean;
@Output() datagridEvent;
@Input() pageSize: number;
public disableFirstPageButton: Boolean;
public disablePreviousPageButton: Boolean;
public disableNextPageButton: Boolean;
public disableLastPageButton: Boolean;
public pageSizeForGrid: number;
public currentPageNumber: number;
public totalRows: number;
public totalPages: number;
public itemNumberBegin: number;
public itemNumberEnd: number;
constructor() {
this.sorter = new DataGridSorter();
this.datagridEvent = new EventEmitter();
this.disableNextPageButton = false;
this.disableLastPageButton = false;
this.disableFirstPageButton = false;
this.disablePreviousPageButton = false;
this.disableFirstPageButton = true;
this.disablePreviousPageButton = true;
this.pageSizes.push(5);
this.pageSizes.push(10);
this.pageSizes.push(15);
this.pageSizeForGrid = 15;
this.sortColumn = "";
this.sortAscending = false;
this.sortDesending = false;
}
public ngOnInit() {}
public databind(transactionalInformation: TransactionalInformation) {
this.currentPageNumber = transactionalInformation.currentPageNumber;
this.totalPages = transactionalInformation.totalPages;
this.totalRows = transactionalInformation.totalRows;
this.itemNumberBegin = ((this.currentPageNumber - 1) * this.pageSize) + 1;
this.itemNumberEnd = this.currentPageNumber * this.pageSize;
if (this.itemNumberEnd > this.totalRows) {
this.itemNumberEnd = this.totalRows;
}
this.disableNextPageButton = false;
this.disableLastPageButton = false;
this.disableFirstPageButton = false;
this.disablePreviousPageButton = false;
if (this.currentPageNumber == 1) {
this.disableFirstPageButton = true;
this.disablePreviousPageButton = true;
}
if (this.currentPageNumber == this.totalPages) {
this.disableNextPageButton = true;
this.disableLastPageButton = true;
}
}
public sortData(key) {
let sortInformation: DataGridSortInformation = this.sorter.sort(key, this.rows);
if (this.sortColumn != key) {
this.sortAscending = true;
this.sortDesending = false;
this.sortColumn = key;
}
else {
this.sortAscending = !this.sortAscending;
this.sortDesending = !this.sortDesending;
}
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "Sorting";
eventInformation.Direction = sortInformation.Direction;
eventInformation.SortDirection = sortInformation.SortDirection;
eventInformation.SortExpression = sortInformation.Column;
this.datagridEvent.emit({
value: eventInformation
});
}
public selectedRow(i: number) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "ItemSelected";
eventInformation.ItemSelected = i;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonClicked(buttonName: string, i: number) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "ButtonClicked";
eventInformation.ButtonClicked = buttonName;
eventInformation.ItemSelected = i;
this.datagridEvent.emit({
value: eventInformation
});
}
public pageSizeChanged(newPageSize) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PageSizeChanged";
this.pageSize = parseInt(newPageSize) + 0;
eventInformation.PageSize = this.pageSize;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonNextPage() {
let currentPageNumber = this.currentPageNumber + 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonPreviousPage() {
this.currentPageNumber = this.currentPageNumber - 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonFirstPage() {
this.currentPageNumber = 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonLastPage() {
this.currentPageNumber = this.totalPages;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
}
Consuming the Data Grid
The Customer Inquiry page for the sample application will consume the data grid component. The only thing needed in the Customer Inquiry HTML template is adding the <datagrid>
selector tag inside the template and assigning the input parameters for both the rows and columns from properties of the Customer Inquiry component.
// customer-inquiry.component.html
<h4 class="page-header">{{title}}</h4>
<div class="form-horizontal" style="margin-bottom:25px;">
<div style="width:20%; float:left; padding-right:1px;">
<input type="text" class="form-control" placeholder="Customer Code"
[(ngModel)]="customerCode" (ngModelChange)="customerCodeChanged($event)" />
</div>
<div style="width:20%; float:left; padding-right:1px;">
<input type="text" class="form-control" placeholder="Company Name"
[(ngModel)]="companyName" (ngModelChange)="companyNameChanged($event)" />
</div>
<div style="float:left; padding-right:1px; padding-left:5px;">
<button class="btn btn-primary" (click)="reset()">
<b>Reset Search</b>
</button>
</div>
<div style="float:left; padding-right:1px; padding-left:5px;">
<button class="btn btn-primary" (click)="search()">
<b>Submit Search</b>
</button>
</div>
<div style="float:right; padding-left:5px;">
<label><input type="checkbox"
[(ngModel)]="autoFilter"> Auto Filtering Search</label>
</div>
</div>
<br clear="all" />
<datagrid [rows]="customers"
[columns]="columns"
[pageSize]="pageSize"
(datagridEvent)="datagridEvent($event)">
</datagrid>
<br style="clear:both;" />
<div>
<alertbox [alerts]="alerts" [messageBox]="messageBox"></alertbox>
</div>
In the CustomerInquiryComponent
, a reference is made to the DataGrid
through the directives property of the @Component
directive. In the OnInit
event, the columns collection is populated with the columns to be displayed in the data grid. The columns array is a collection of DataGridColumn
objects that allows you to also specify any additional formatting.
The CustomerInquiryComponents
binds to the DataGrid
event emitter through the output parameter of the data grid component. Because it is bound to the data grid component, the datagridEvent(event)
is executed in the consuming CustomerInquiry
component. This is an example of executing code in a consuming component through an event emitter without the consuming component needing to subscribe to the event emitter.
When events are fired back into the CustomerInquiry
component. the component executes the needed method for paging, sorting, and filtering purposes with http calls back to the server to re-populate the data grid with new data.
When data is returned back from the server request, the data is data-bound back to data grid and the pager is updated with new page information. The customer-inquiry.component.ts code below accesses the data grid component method databind
to update the data grid pager through a @ViewChild
annotation.
Since all components in Angular 2 have classes, you might want to call methods on these classes or access properties of these classes from a parent component. This requires access to the child component. To get access to a component and its methods, the @ViewChild
annotation is used.
The Customer Inquiry Component also supports type-ahead functionality where the data grid is filtered on-the-fly as the user types in customer name information. This is supported with the use of the Angular 2 setTimeout()
callback function to allow for a slight delay between keystrokes before executing a call to the server to get data.
import { Component, OnInit, EventEmitter, Output, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { DataGridColumn, DataGridButton, DataGridEventInformation }
from '../shared/datagrid/datagrid.core';
import { DataGrid } from '../shared/datagrid/datagrid.component';
import { AlertService } from '../services/alert.service';
import { CustomerService } from '../services/customer.service';
import { AlertBoxComponent } from '../shared/alertbox.component';
import { Customer } from '../entities/customer.entity';
import { TransactionalInformation } from '../entities/transactionalinformation.entity';
export var debugVersion = "?version=" + Date.now();
@Component({
templateUrl: 'application/customer/customer-inquiry.component.html' + debugVersion,
directives: [DataGrid, AlertBoxComponent],
providers: [AlertService]
})
export class CustomerInquiryComponent implements OnInit {
@ViewChild(DataGrid) datagrid: DataGrid;
public title: string = 'Customer Inquiry';
public customers: Customer[];
public columns = [];
public alerts: Array<string> = [];
public messageBox: string;
public totalRows: number;
public currentPageNumber: number = 1;
public totalPages: number;
public pageSize: number;
public companyName: string;
public customerCode: string;
private sortDirection: string;
private sortExpression: string;
public autoFilter: Boolean;
public delaySearch: Boolean;
public runningSearch: Boolean;
constructor(private alertService: AlertService, private customerService: CustomerService,
private router: Router) {
this.currentPageNumber = 1;
this.autoFilter = false;
this.totalPages = 0;
this.totalRows = 0;
this.pageSize = 15;
this.sortDirection = "ASC";
this.sortExpression = "CompanyName";
}
public ngOnInit() {
this.columns.push(new DataGridColumn('customerCode', 'Customer Code',
'[{"width": "20%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('companyName', 'Company Name',
'[{"width": "30%" ,
"hyperLink": true, "disableSorting": false}]'));
this.columns.push(new DataGridColumn('city', 'City',
'[{"width": "20%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('zipCode', 'Zip Code',
'[{"width": "15%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('dateUpdated', 'Date Updated',
'[{"width": "15%" ,
"disableSorting": false, "formatDate": true}]'));
this.executeSearch();
}
private executeSearch(): void {
if (this.runningSearch == true) return;
let miliseconds = 500;
if (this.delaySearch == false) {
miliseconds = 0;
}
this.runningSearch = true;
setTimeout(() => {
let customer = new Customer();
customer.customerCode = this.customerCode;
customer.companyName = this.companyName;
customer.pageSize = this.pageSize;
customer.sortDirection = this.sortDirection;
customer.sortExpression = this.sortExpression;
customer.currentPageNumber = this.currentPageNumber;
this.customerService.getCustomers(customer)
.subscribe(
response => this.getCustomersOnSuccess(response),
response => this.getCustomersOnError(response));
}, miliseconds)
}
private getCustomersOnSuccess(response: Customer): void {
let transactionalInformation = new TransactionalInformation();
transactionalInformation.currentPageNumber = this.currentPageNumber;
transactionalInformation.pageSize = this.pageSize;
transactionalInformation.totalPages = response.totalPages;
transactionalInformation.totalRows = response.totalRows;
transactionalInformation.sortDirection = this.sortDirection;
transactionalInformation.sortExpression = this.sortExpression;
this.customers = response.customers;
this.datagrid.databind(transactionalInformation);
this.alertService.renderSuccessMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
this.runningSearch = false;
}
private getCustomersOnError(response): void {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
this.runningSearch = false;
}
public datagridEvent(event) {
let datagridEvent: DataGridEventInformation = event.value;
if (datagridEvent.EventType == "PagingEvent") {
this.pagingCustomers(datagridEvent.CurrentPageNumber);
}
else if (datagridEvent.EventType == "PageSizeChanged") {
this.pageSizeChanged(datagridEvent.PageSize);
}
else if (datagridEvent.EventType == "ItemSelected") {
this.selectedCustomer(datagridEvent.ItemSelected);
}
else if (datagridEvent.EventType == "Sorting") {
this.sortCustomers(datagridEvent.SortDirection, datagridEvent.SortExpression);
}
}
private selectedCustomer(itemSelected: number) {
let rowSelected = itemSelected;
let row = this.customers[rowSelected];
let customerID = row.customerID;
this.router.navigate(['/customer/customermaintenance', { id: customerID }]);
}
private sortCustomers(sortDirection: string, sortExpression: string) {
this.sortDirection = sortDirection;
this.sortExpression = sortExpression;
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
private pagingCustomers(currentPageNumber: number) {
this.currentPageNumber = currentPageNumber;
this.delaySearch = false;
this.executeSearch();
}
private pageSizeChanged(pageSize: number) {
this.pageSize = pageSize;
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public reset(): void {
this.customerCode = "";
this.companyName = "";
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public search(): void {
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public companyNameChanged(newValue): void {
if (this.autoFilter == false) return;
if (newValue == "") return;
this.companyName = newValue;
this.currentPageNumber = 1;
this.delaySearch = true;
setTimeout(() => {
this.executeSearch();
}, 500)
}
public customerCodeChanged(newValue): void {
if (this.autoFilter == false) return;
if (newValue == "") return;
this.customerCode = newValue;
this.currentPageNumber = 1;
this.delaySearch = true;
setTimeout(() => {
this.executeSearch();
}, 500)
}
}
Throughout the sample application, I added a date time stamp to end of the url of the templateUrl
property. This helps break the browser cache while developing the application. I was using browser syncing functionality when developing this application that automatically watches for changes in my TypeScript and HTML files and would then automatically refresh the browser for me. Later in the production build, this date time stamp is removed through a Gulp task.
export var debugVersion = "?version=" + Date.now();
@Component({
templateUrl: 'application/customer/customer-inquiry.component.html' + debugVersion,
directives: [DataGrid, AlertBoxComponent],
providers: [AlertService]
})
Entities
Finally for this application, I needed to create classes that represented the format of all the data coming back from the server. I created separate entity classes for each entity. In Angular 2 on the client side, you can consider these entities as view model
entities because they don't necessarily match the format of the server side data model. View model classes generally have more or less data properties than their server-side counterparts.
One of the cool things with TypeScript is that you can use inheritance by extending your classes with the extends
clause. The TransactionalInformation
entity contains common information from the server that all the entities in the application will need to reference. The user class extends the TransactionalInformation
class so the properties are included in the User
class.
import { TransactionalInformation } from './transactionalinformation.entity';
export class User extends TransactionalInformation {
public userID: number;
public firstName: string;
public lastName: string;
public emailAddress: string;
public addressLine1: string;
public addressLine2: string;
public city: string;
public state: string;
public zipCode: string;
public password: string;
public passwordConfirmation: string;
public dateCreated: Date;
public dateUpdated: Date;
}
Conclusion
Let me say that after developing an Angular 2 application with TypeScript, I have to say that I love both Angular 2 and TypeScript. It seems like the perfect utopia for client side development. Angular 2 has been rewritten from scratch and it feels so much cleaner and simpler than it's predecessor Angular 1. Using TypeScript is an added benefit that provides for a greater development experience and a better looking code base. If you have experience with Angular 1, you'll find that the learning curve to Angular 2 to be minimal. When deploying an Angular 2 application you'll also notice how much more streamlined and efficient Angular 2 runs over Angular 1 with faster page loads and more efficient data binding. I'm looking forward to the final release of Angular 2.
History
- 13th August, 2016: Initial version