Angular 2 & .NET Core Developing Web App from Scratch Part 2: Implementing Front-end Part
Great article to learn how you can create web application from scratch using Angular2 & .NET CORE WEB API
Contents
Introduction
After preparing the server part, we will start to build the client part (front-end part) using Angular 2 framework.
Our application is a single page application composed by four modules:
- home: is the main page, used to expose available products to clients.
- Login: is the authentication page, that allows users to log into the application and have access to the back-office side.
- Subscription: is the subscription page, which provides a form to create a new user's account.
- Product Management: This module provides a form to insert new products into the database.
In this article, I relied on the following links:
- Angular2 Documentation
- Angular2 material
- Angular2 cookies
- CRUD in ASP.NETCore MVC with Angular 2 and Web API
Background
To better understand this demo, it is preferable that you have a good knowledge about:
- Programming in C#, JavaScript and HTML
- Angular 2
- MVC architecture
- Data Binding
- Entity Framework
- Visual Studio Code (optional)
Prerequisites
Using the Code
A) Create and Configure Angular 2 Project
First, use CMD to create a new folder (new project) named 'angular2fromscratch
' in which you download the Angular 2 project from the offical Angular 2 documentation.
Later, you should create the same configuration files (based on the official Angular 2 documentation):
- package.json
- tsconfig.json
- typings.json
Next, you should install TypeScript, typings, webpack, cookies service and angular2-material
using npm
:
Npm install –g typescript
Npm install -g typings
Npm install angular2-cookie
npm install @angular2-material/sidenav
npm install @angular2-material/input
npm install @angular2-material/button
npm install @angular2-material/core
npm install @angular2-material/card
npm install @angular2-material/icon
npm install @angular2-material/toolbar
npm install @angular2-material/progress-circle
npm install @angular2-material/sidenav
The final treeview
should look like this:
Before you start your implementation, you should make some changes on the systemjs.config.js file, to define the alias for the Angular 2 packages:
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser':
'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic':
'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular2-material/input': 'npm:@angular2-material/input/input.umd.js',
'@angular2-material/core': 'npm:@angular2-material/core/core.umd.js',
'@angular2-material/card': 'npm:@angular2-material/card/card.umd.js',
'@angular2-material/button': 'npm:@angular2-material/button/button.umd.js',
'@angular2-material/icon': 'npm:@angular2-material/icon/icon.umd.js',
'@angular2-material/toolbar': 'npm:@angular2-material/toolbar/toolbar.umd.js',
'@angular2-material/progress-circle':
'npm:@angular2-material/progress-circle/progress-circle.umd.js',
'@angular2-material/sidenav': 'npm:@angular2-material/sidenav/sidenav.umd.js',
'angular2-cookie': 'npm:angular2-cookie',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
},
'angular-in-memory-web-api': {
main: './index.js',
defaultExtension: 'js'
},
'angular2-cookie': {
main: './core.js',
defaultExtension: 'js'
},
}
});
})(this);
B) Setup Angular 2 Project
In the app folder, create the following files:
- app.module.ts
This file is used to:
- Define routes using Router Module: "
RouterModule
" - Import needed Angular 2 modules via the word key: "
imports
" - Declare components via the word key: "
declarations
" - Declare services via the word key: "
providers
", - Specify the root components to include into index.html file via the word key: "
bootstrap
"
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { MdInputModule } from '@angular2-material/input'; import { MdCardModule } from '@angular2-material/card'; import { MdButtonModule } from '@angular2-material/button'; import { MdIconModule } from '@angular2-material/icon'; import { MdToolbarModule } from '@angular2-material/toolbar'; import { MdIconRegistry } from '@angular2-material/icon'; import { CookieService } from 'angular2-cookie/services/cookies.service'; import { AppComponent } from './app.component'; import { loginComponenent } from './app.loginComponenent'; import { homeComponenent } from './app.homeComponenent'; import { subscriptionComponenent } from './app.subscriptionComponenent'; import { productManageComponent } from './app.productManageComponent'; import { PageNotFoundComponent } from './app.pageNotFoundComponent'; import { MdProgressCircleModule } from '@angular2-material/progress-circle'; import { MdSidenavModule } from '@angular2-material/sidenav'; import { AuthentificationService } from './services/authentificationService'; import { ManageProductService } from './services/manageProductService' const appRoutes: Routes = [ { path: 'home', component: homeComponenent }, { path: 'login', component: loginComponenent }, { path: 'subscription', component: subscriptionComponenent }, { path: 'productManagement', component: productManageComponent }, { path: '', redirectTo: '/home', pathMatch: 'full'}, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ FormsModule , BrowserModule,MdSidenavModule, MdProgressCircleModule, MdInputModule, MdToolbarModule, MdCardModule, MdButtonModule, MdIconModule, RouterModule.forRoot(appRoutes)], declarations: [ AppComponent, loginComponenent, homeComponenent, subscriptionComponenent, productManageComponent, PageNotFoundComponent], providers: [ CookieService, MdIconRegistry, AuthentificationService, ManageProductService ], bootstrap: [ AppComponent ] }) export class AppModule { }
- Define routes using Router Module: "
- app.component.ts
This file is used as code behind for the app.html template, in which you will have implementation of:
constructor
: used to initialize our master page (app.html), and to declare the router listener that controls user navigation depending on current session value (session is managed by cookies).logOut
event: this event is used to free the current session by clearing cookies, and redirect user to home page.
import { Component } from '@angular/core'; import {MdToolbar} from '@angular2-material/toolbar'; import {CookieService} from 'angular2-cookie/core'; import {Router, NavigationStart, NavigationEnd} from '@angular/router' @Component({ selector: 'my-app', templateUrl: 'app/templates/app.html', }) export class AppComponent { title = 'Angular 2 Application'; showMenu : boolean = false; login : string = ""; constructor(private _cookieService: CookieService, private router : Router){ this.showMenu = false; router.events.subscribe((val) => { // see also if(val instanceof NavigationStart) { this.login = this._cookieService.get("login"); if(this.login != null && this.login != ""){ this.showMenu = true; if(val.url.endsWith("login") || val.url.endsWith("subscription")){ router.navigateByUrl("/home"); } } else{ this.showMenu = false; if(val.url.endsWith("productManagement")){ router.navigateByUrl("/home"); } } } }); } public logOut(right){ this.showMenu = false; right.close(); this._cookieService.remove("login"); location.reload(); //this.router.navigateByUrl("/home"); } }
- app.homeComponent.ts
Constitute the code behind for our
home
template, in which you will have implementation of:ngOnInit
event: refresh the product list on each page initializationRemove
event: deletes an existing product (identified by its "unique key") by calling "Delete
method" of "_service
variable"loadProducts event
: load all available products form remote server via the call ofloadProducts
method of_service
variable
import { Component , OnInit} from '@angular/core'; import { Product } from './model/product' import { ManageProductService } from './services/manageProductService' import {CookieService} from 'angular2-cookie/core'; @Component({ templateUrl: 'app/templates/home.html' }) export class homeComponenent extends OnInit { title = 'Products'; showProgress : boolean = false; showError : boolean = false; showEmpty : boolean = false; isLogged : boolean = false; listProduct : Product[]; constructor(private _service : ManageProductService, private _cookieService: CookieService){ super(); console.log("constructor"); } ngOnInit() { this.loadProducts(); this.isLogged = (this._cookieService.get("login")!= null)?true: false; } public Remove(id: number) { var result = confirm("Do you want to continue the suppression process ?"); let _self = this; _self.showError = false; _self.showEmpty = false; if (result == true) { this._service.deleteProduct(id).then(function(){ _self.listProduct = _self.listProduct.filter(value=> { return (value.id != id); }); if(_self.listProduct.length==0){ _self.showEmpty = true; } }).catch(function(){ _self.showError = true; }); } else { } } public loadProducts(){ let _self = this; _self.showError = false; _self.showEmpty = false; _self.showProgress = true; _self._service.loadProducts().then(function(response){ _self.showProgress = false; if(response.length >0){ _self.listProduct = response; }else{ _self.showEmpty = true; } }).catch(function(error: any){ _self.showProgress = false; _self.showError = true; }); } }
- app.loginComponent.ts
Constitute the code behind for the
login
template, in which you will have the implementation of:tryToConnect
event: open a new session with server using user credential (email, password)
import { Component } from '@angular/core'; import {Router} from '@angular/router' import { User } from './model/user'; import { AuthentificationService } from './services/authentificationService'; import {CookieService} from 'angular2-cookie/core'; @Component({ templateUrl: 'app/templates/login.html' }) export class loginComponenent { public title = 'Login'; public model : User = new User(); constructor(private _service : AuthentificationService, private _cookieService:CookieService, private router: Router){ } tryToConnect(){ let _self = this; _self._service.Login(_self.model).then(function (response){ _self._cookieService.put("login", _self.model.login); _self.router.navigateByUrl('/home'); }) .catch(function(error : any){ alert("Fail to login"); }); } }
- app.productManageComponent.ts
Constitute the code behind for our
productManagement
template, in which you will have the implementation of:addNewProduct
event: called when the user clicks on the submit button for submitting the form data to register a new product. It will call theaddProduct
method of the_service
variable.
import { Component } from '@angular/core'; import { Product } from './model/Product'; import { ManageProductService } from './services/manageProductService'; @Component({ templateUrl: 'app/templates/productManagement.html' }) export class productManageComponent { title = 'Manage Product'; model : Product; constructor(private _service : ManageProductService){ this.model = new Product(); } onChange(event) { var file = event.srcElement.files[0]; this.model.pictureFile = file; } public addNewProduct(){ let _self = this; let formData = new FormData(); formData.append("title",_self.model.title); formData.append("fullDescription",_self.model.fullDescription); formData.append("price",_self.model.price); formData.append("file",_self.model.pictureFile, _self.model.pictureFile.name); _self._service.addProduct(formData).then(function(response){ alert("new product was successfully created"); }).catch(function(error: any){ alert("Server error"); }); } }
- app.subscriptionComponent.ts
Constitute the code behind for our
singup
template, in which you will have the implementation of:Subscription
event: raised when user try to submit new user details. It will call theaddProduct
method of the_service
variable.
import { Component } from '@angular/core'; import { User } from './model/user'; import { AuthentificationService } from './services/authentificationService'; import { NgModule } from '@angular/core'; @Component({ templateUrl: 'app/templates/singup.html' }) export class subscriptionComponenent { title = 'Subscription'; public model : User; constructor(private _service : AuthentificationService){ this.model = new User(); } public Subscription(){ let _self = this; this._service.Signup(this.model).then(function(response){ alert("Congratulation, the user with login : "+ _self.model.login + " has been created"); }) .catch(function(error : any){ alert("Fail to create new user"); }); } }
Next, you should create the different folders and files:
1) model folder:
- product.ts
Used to deserialize JSON data into a
product
object.export class Product{ id : number; title : string = ''; fullDescription : string = ''; price : number = 0; picture : string; pictureFile : any; }
- user.ts
Used to deserialize JSON data into a
user
object.export class User{ login : string = ''; pwd : string = ''; pwdConfirmation : string = ''; }
2) services folder:
- authentificationService.ts
Implements needed methods to ensure user authentication and user registration:
Login
: invokes an external web service "api/Services/Login" to create session with server.Signup
: calls a remote web service "api/Services/Subscription" to register the user.
import { Injectable } from '@angular/core'; import { Http, Response, RequestOptions, Headers } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import {User} from '../model/user'; @Injectable() export class AuthentificationService { constructor( private _http : Http){ } Login(model : User): Promise<any> { let _url = "http://localhost:5000/api/Services/Login"; let bodyString = JSON.stringify(model); let headers = new Headers ({ 'Content-Type': 'application/json' }); // ... Set content type // to JSON let options = new RequestOptions({ headers: headers }); return this._http.post(_url, bodyString, options) .toPromise(); } Signup(model : any): Promise<any> { let _url = "http://localhost:5000/api/Services/Subscription"; let bodyString = JSON.stringify(model); let headers = new Headers ({ 'Content-Type': 'application/json' }); // ... Set content // type to JSON let options = new RequestOptions({ headers: headers }); return this._http.post(_url, bodyString, options) .toPromise(); } protected handleErrorPromise(error: any): Promise<void> { try { error = JSON.parse(error._body); } catch (e) { } return Promise.reject(error); } }
- manageProductService.ts
This class offers services to manage product such as:
loadProducts
: loads the available products list by calling an external web service via "api/Services/GetProducts"addProduct
: it calls an external web service "api/Services/AddProduct" to create a new productdeleteProduct
: deletes a specific product from database by calling an existing web service "api/Services/DeleteProduct/"
import { Injectable } from '@angular/core'; import { Http, Response, RequestOptions, Headers } from '@angular/http'; import { Product} from '../model/Product'; import 'rxjs/add/operator/toPromise'; @Injectable() export class ManageProductService { constructor(private _http: Http) { } loadProducts(): Promise<Product[]> { let _url = "http://localhost:5000/api/Services/GetProducts"; return this._http.get(_url) .toPromise() .then(response => this.extractArray(response)); } addProduct(formData: any): Promise<any>{ let _url = "http://localhost:5000/api/Services/AddProduct"; return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open('POST', _url, true); xhr.onload = function (e) { resolve(JSON.parse(xhr.response)); }; xhr.onerror = function (e) { reject(e); } xhr.send(formData); }); } deleteProduct(id: number) { let _url = "http://localhost:5000/api/Services/DeleteProduct/"; return this._http.delete(_url+'?id=' + id).toPromise(); } protected extractArray(res: Response, showprogress: boolean = true) { let data = res.json(); return data || []; } }
3) templates folder:
- app.html
This is an HTML template and is used as a master page. It contains:
md-toolbar
: it considered as a naviation menu, from which you can navigate to home, login and subscription pages.md-sidenav
: is the right-side navigation menu, which displays information about:- email of current user
- navigation button: used to redirect to the
productManagement
page - logout button: to delete current session and redirect to the
home
page
router-outlet
: is placeholder used to display specific view based on router state (read more about router-outlet).
<md-toolbar >
<button md-icon-button class="md-raised md-primary" routerLink="/home">
<md-icon >home</md-icon>
</button>
<span>{{title}}</span>
<span flex></span>
<a md-button routerLink="/login" *ngIf="showMenu == false"
routerLinkActive="active">
<i md-icon></i>Login
</a>
<a md-button routerLink="/subscription" *ngIf="showMenu == false"
routerLinkActive="active">
<i md-icon></i>Sign up
</a>
<button md-icon-button *ngIf="showMenu == true"
class="md-raised md-primary" (click)="right.toggle()">
<md-icon class="md-24">list</md-icon>
</button>
</md-toolbar>
<md-sidenav-layout>
<md-sidenav #right align="end" layout-padding>
<h5 style="text-align:center"><md-icon>account_circle</md-icon></h5>
<h6 style="text-align:center"> {{login}}</h6>
<hr>
<div style="text-align:center">
<button md-button routerLink="/productManagement"
class="md-icon-button" style="width: 100%" (click)="right.close()"
routerLinkActive="active">
<h5> Manage</h5>
</button>
</div>
<div style="text-align:center">
<button md-button class="md-icon-button"
style="width: 100%"
(click)="logOut(right)" routerLinkActive="active">
<h5>Logout</h5>
</button>
</div>
</md-sidenav>
<div layout-padding>
<router-outlet></router-outlet>
</div>
</md-sidenav-layout>
- home.html
Through this page, the user can see a list of available products with details. If the user is connected, it will have a possibility to remove some products from the current list.
<div > <h1>{{title}}</h1> <div class="row"> <md-progress-circle color="primary" mode="indeterminate" *ngIf="showProgress" [style.width]="'40px'" [style.margin]="'0 auto'" ></md-progress-circle> <div *ngIf="showEmpty" class="alert alert-info"> <strong>Info :</strong> Empty Result </div> <div *ngIf="showError" class="alert alert-danger"> <strong>Error :</strong> Problem happened in server side. </div> <div> <div class="row" > <div *ngFor="let obj of listProduct" class="col col-lg-4" style="margin-top:1px; " > <md-card > <md-card-title-group> <img md-card-md-image [src]="obj.picture"> <md-card-title>{{obj.title}}</md-card-title> <md-card-subtitle>Price : {{obj.price}} $</md-card-subtitle> </md-card-title-group> <md-card-actions *ngIf="isLogged == true" style="text-align: center"> <button md-icon-button class="md-primary" (click)="Remove(obj.id)" > <md-icon class="md-24" >delete</md-icon> </button> </md-card-actions> </md-card> <div class="clearfix" ></div> </div> </div> </div>
- login.html
This page offers a connection form, that allows the user to enter his credential (email and password) and to open new session.
<div> <h1>{{title}}</h1> <div > <form> <div> <label class="vSpace"> Login :</label> <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" > </md-input> </div> <div> <label class="vSpace"> Password : </label> <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" > </md-input> </div> <div> <button type="button" md-raised-button [disabled]="(model.login == '' || model.pwd =='')" (click)="tryToConnect()" class="md-primary" value="submit" > submit </button> </div> </form> </div> </div>
- productManagement.html
This page offers user possibility to add a new product through using a form. He must provide information about: product Name (title), description, price, thumbnail (Picture).
<div> <h1>{{title}}</h1> <div > <form> <div> <label class="vSpace"> Title :</label> <md-input type="text" name="title" [(ngModel)]="model.title" placeholder="title"> </md-input> </div> <div> <label class="vSpace"> Full description :</label> <md-input type="text"name="fullDescription" [(ngModel)]="model.fullDescription" placeholder="Description"> </md-input> </div> <div> <label class="vSpace"> Price ($) : </label> <md-input type="number" name="price" [(ngModel)]="model.price" placeholder="Price" ></md-input> </div> <div> <label class="vSpace"> Piture : </label> <label class="btn btn-default btn-file"> Load File <input type="file" id="file" name="pictureFile" (change)="onChange($event)" class="md-primary" class="hidden"> </label> </div> <div> <button type="button" md-raised-button [disabled]="(model.title == '' || model.fullDescription =='')" (click)="addNewProduct()" class="md-primary" value="submit" > Add </button> </div> </form> </div> </div>
- singup.html
Through this page, any anonymous user can create their own account, to have the privileges to access the back-office of application.
To create a new account, the user should complete the subscription form by filling following information: login, password.
<div> <h1>{{title}}</h1> <div> <form id="newProductForm" > <div> <label class="vSpace">Login :</label> <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" > </md-input> </div> <div> <label class="vSpace">Password :</label> <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" > </md-input> </div> <div> <label class="vSpace">Confirm password :</label> <md-input type="password" name="pwdConfirmation" [(ngModel)]="model.pwdConfirmation" placeholder="Confirm password" > </md-input> </div> <div> <button md-raised-button type="button" [disabled]="(model.login == '' || model.pwd =='' || model.pwdConfirmation != model.pwd)" (click)="Subscription()" class="md-primary" value="submit" > submit </button> </div> </form> </div> </div>
To run the demo, you should write the following command-line using CMD
, but first be sure that you are in the root directory of your application:
npm start
: to transpile TS files to JavaScript files and start application.
When you try to open application via the given url (http://localhost:3000), you will get the below result:
To get this result, you should start your server application (see Part 1 of this article: Server side implementation). Make sure your web service is accessible from this address: http://localhost:5000.
References
- Angular 2 Documentation
- Angular 2 material
- Angular 2 cookies
- CRUD in ASP.NET Core MVC with Angular 2 and Web API
Points of Interest
I hope that you appreciated this series of articles. Try to download the source code, and I'm waiting for your questions and comments.
History
- v1 29th January, 2017: Initial version