This is the second part of an article that shows how to implement the front end part of developing a web app from scratch using Angular 2 and .NET Core.
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:
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: {
'npm:': 'node_modules/'
},
map: {
app: 'app',
'@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',
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
},
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 { }
- 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) => {
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();
}
}
- 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 initialization Remove
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 of loadProducts
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 the addProduct
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 the addProduct
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' });
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' });
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"
address addProduct
: it calls an external web service "api/Services/AddProduct" to create a new product deleteProduct
: 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:
<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
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