Monday, 25 February 2019

Angular CRUD


In this post, I am going to build an Angular application to demonstrate CRUD (Create, Read, Update and Delete) operation step-by-step with an example. I have taken an example of CRUD operation for product. I have tried to make it real life example as much as possible.
This post will help you to learn following:
1.    Authentication using Token based authentication
2.    Template Driven forms and Reactive forms
3.    Routing and navigation
4.    Authorization to access  [/Admin] route using AuthGaurd
5.    Use of HttpRequestInterceptor to modify http request.
6.    Children route for implementing nested routes
7.    Use of cookie to remember user credentials
8.    Use of Toastr to display notification and messages
9.    Image upload
10.Creation of Services to use HttpClientModule for http request

Here are some Terms that you may not familiar with.
HttpRequestInterceptor: Angular has HttpInterceptor that Intercepts HttpRequest or HttpResponse and handles them. So, HttpRequestInterceptor intercepts all the HttpRequest and modify to append header with bearer access token to authenticate the user.
Children Route: It allows us to create child routes that will be nested underneath a certain route.
AuthGuard: It is a class that implement canActivate interface to be a guard for deciding if route can be activated. It will be run any time someone tries to access protected route like the /Admin route. If the user is authenticated, they can access the route.


Application structure


In the above application structure, I have created following folders and files:
1. Admin folder: It contains all admin related components and modules.
         1. admin-layout folder: It contains component for admin screen layout
2. ManageProduct folder: It contains component to list, add, edit and delete product. It contains two folder, one is AddProduct folder that contains add-product component that is used for both add/edit of product and ProductList folder that contains product-list component to list all the products added.
It also contains index.ts file. The purpose of this file is to import all the component/service/directive etc. available in current folder in the module or component using single import statement instead of writing individual line for each component/service/directive etc.
3. admin.routes.ts: It contains admin routes array.
4. admin-routing.module.ts: It is admin routing module that handles admin related routing that import admin routes as child routes for lazy loading of admin module.
5. admin.module.ts: It is the admin module that handles all the components/services/directives etc. related to admin section.
2. APIService folder: It contains services for authentication, GET, POST, PUT, DELETE method for product and category related http request.
3. Login folder: It contains login component for user authentication.
4. Models folder: It contains model classes for User, Product and Category.
5. Shared folder: It contains AuthGaurd and HttpRequestInterceptor class.
6. app.global.ts: It contains global values to be used across the application.
7. app.routes.ts, app.routing.module.ts, app.module.ts: It contains the root level routes, routing module and bootstrap module.


Let’s create the above application. First create a basic angular application using angular CLI. Use following link for help:
We are going to use following packages so, install these packages using angular CLI.
npm install bootstrap
npm install jquery
npm install font-awesome
npm install ngx-cookie-service
npm install ngx-toastr
npm install @angular/animations

First create a service for global values. Create following file and write below code. It contains our Web API BaseURL. We can use this service for any other global values.

app.global.ts
import { Injectable } from '@angular/core';
@Injectable({
    providedIn: "root"
})
export class GlobalValues {   
    API_BaseURL: string = "http://localhost:21453";
    API_URL: string = this.API_BaseURL + "/api/";
}

Create model.ts file in Models folder and create following Category, Product and User Classes.

Models\model.ts
export class Category {
    CategoryID: number;
    CategoryName: string;
    ProductCount: number;
}

export class Product {
    ProductID: number;
    CategoryID: number;
    CategoryName: string;
    ProductName: string;
    Brand: string;
    ProductModel: string;
    ProductFeature: string;
    ProductSummary: string;
    ProductConfiguration: string;
    MRP: number;
    SalePrice: number;
    TaxPerc: number;
    ImageURL: string;
}

export class User {
    Username: string;
    UserToken: string;
}


We start with Authentication module. For this, we need login screen and authentication service to validate user credentials. Let’s create authentication service.
npm g service APIService/authentication –spec=false --flat=true
Here, It contains login() method to authenticate user and store user info in localStorage and logout() to remove user info from localStorage. HttpClient, HttpHeaders are imported to make http request with header, GlobalValues is imported to get API_BaseURL global variable, User class to hold user name and access token and map operator from rxjs to map the response. If the authentication successful then the user name and accesstoken is stored  in User class instance and stored in localStorage using currentUser key. This currentUser will be used to check whether user is authenticated and for further subsequent request, user will be validated using this access token stored in currentUser.

APIService\authentication.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'
import { GlobalValues } from '../app.global';
import { User } from '../Models/model';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
 //To store user info obtained from response
  currentUser: User;
  constructor(private http: HttpClient, private global: GlobalValues) { }

  login(username: string, password: string) {
    let userData = "username=" + username + "&password=" + password + "&grant_type=password";
    let reqHeaders = new HttpHeaders({
      'Content-Type': 'application/x-www-form-urlencoded', 'No-Auth': 'true'
    });
    return this.http.post<any>(this.global.API_BaseURL + "/token", userData, { headers: reqHeaders }).pipe(map(user => {
      this.currentUser = { Username: user.userName, UserToken: user.access_token };
//Information is stored in localStorage for future use
      localStorage.setItem("currentUser", JSON.stringify(this.currentUser));
    }));
  }

  logout() {
    localStorage.removeItem("currentUser");
  }
}


Now, create login component that accept credentials from user and authenticate.

npm g component Login/login –spec=false --flat=true

Here, I have used reactive forms to create and validate login form fields.
Login screen UI

Login Screen
Login\login.components.html
<div class="login-bg mt-5" style="height: 500px;">
  <div class="container">
    <div class="row">
      <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
        <div class="card card-signin">
          <div class="card-body">
            <h5 class="card-title text-center">Sign In</h5>
            <form [formGroup]="loginForm" class="form-signin" (ngSubmit)="onSubmit()">
              <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" name="username" formControlName="username" class="form-control"
                  placeholder="Email address" required autofocus>
              </div>
              <div *ngIf="submitted && f.username.errors" class="invalid">
                <div *ngIf="f.username.errors.required">Username is required</div>
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password" formControlName="password" class="form-control"
                  placeholder="Password" required>
              </div>
              <div *ngIf="submitted && f.password.errors" class="invalid">
                <div *ngIf="f.password.errors.required">Password is required</div>
              </div>
              <div class="custom-control custom-checkbox mb-3">
                <input type="checkbox" class="custom-control-input" id="rememberMe" name="rememberMe"
                  formControlName="rememberMe">
                <label class="custom-control-label" for="rememberMe">Remember password</label>
              </div>
              <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Here, I have imported FormBuilder, FormGroup, Validators to create and validate Reactive Form.
CookieService is imported to store the user credential in cookie if user has checked “Remember me” checkbox.
AuthenticationService to make request for authentication.
Router and ActivatedRoute to navigate and read request querystring parameter value.
first to get first item of the response array.
ToastrService to display validation messages.
Title to set browser tab title value.

Login\login.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
import { CookieService } from 'ngx-cookie-service';
import { AuthenticationService } from 'src/app/APIService';
import { Router, ActivatedRoute } from '@angular/router'
import { first } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr'
import { Title } from '@angular/platform-browser';
@Component({
  selector: 'login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  loginForm: FormGroup;
  submitted: boolean = false;
  returnURL: string;

  constructor(
    private title: Title,
    private formBuilder: FormBuilder,
    private cookieService: CookieService,
    private authenticationService: AuthenticationService,
    private route: ActivatedRoute,
    private router: Router,
    private toastr: ToastrService
  ) { }

  ngOnInit() {
    //Set Page title
    this.title.setTitle("Angular CRUD-Login");
    this.loginForm = this.formBuilder.group({
      username: ['', Validators.required],
      password: ['', Validators.required],
      rememberMe: [null]
    });

    // get return url from route parameters or default to '/'
    this.returnURL = this.route.snapshot.queryParams['returnUrl'] || '/admin';

    //If cookie exists set values to controls from cookie
    if (this.cookieService.check("rememberMe")) {
      this.loginForm.controls["username"].setValue(this.cookieService.get("username"));
      this.loginForm.controls["password"].setValue(this.cookieService.get("password"));
      this.loginForm.controls["rememberMe"].setValue(this.cookieService.get("rememberMe"));
    }
  }

  //property to refer Reactive form controls
  get f() { return this.loginForm.controls; }


  onSubmit() {
    this.submitted = true;
    if (this.loginForm.invalid) {
      return;
    }
this.authenticationService.login(this.f.username.value, this.f.password.value).pipe(first()).subscribe(
      data => {
        this.setCookie(this.f.rememberMe.value);
      //navigate to return URL if exists else admin route
        this.router.navigate([this.returnURL]);
      },
      error => {
       //If not authenticated or in case or error, display error message
        this.toastr.error('Username or Password is incorrect', 'Error!');
        this.router.navigate(['/login']);
      }
    );
  }

  //Save or delete cookies based on rememberMe value
  setCookie(rememberMe: boolean) {
    if (rememberMe) {
      this.cookieService.set("username", this.f.username.value);
      this.cookieService.set("password", this.f.password.value);
      this.cookieService.set("rememberMe", this.f.username.value);
    }
    else {
      this.cookieService.delete("username");
      this.cookieService.delete("password");
      this.cookieService.delete("rememberMe");
    }
  }
}

Message display using ToastrService
Message display using Toastr
Add following CSS in styles.css file to apply font-awesome, toastr and other css globally.

styles.css
@import url("../node_modules/font-awesome/css/font-awesome.min.css");
@import url("../node_modules/ngx-toastr/toastr.css");
.cursor-pointer{cursorpointer;}
textarea{resizenone!important;}
.inactive{background-colorlightsalmon!important;}


.invalid{color:red;}


Now, set routes for the application. Here, path is set for login component and admin section. By default, login component will be loaded for root URL.

app.routes.ts
import { Routes } from '@angular/router'
import { LoginComponent } from './Login/login.component';

export const appRoutes: Routes = [
    { path: '', redirectTo: '/login', pathMatch: 'full' },
    { path: 'login', component: LoginComponent },
    //admin path is set for lazy loading of AdminModule(will be created it later)
    { path: 'admin', loadChildren: './Admin/admin.module#AdminModule' },
    { path: '**', redirectTo: '/login', pathMatch: 'full' }
];

Now, register appRoutes in app.routing.module.ts. I have registered the appRoutes in RouterModule.forRoot in imports array and exported the RouterModule in exports array.

app.routing.module.ts
import { NgModule } from '@angular/core'
import {RouterModule} from '@angular/router'
import {appRoutes} from './app.routes'

@NgModule({
    declarations:[],
    exports:[RouterModule],
    imports:[RouterModule.forRoot(appRoutes)]
})
export class AppRoutingModule
{   
}

Let set a placeholder in app.component.html file where components will render in case of routing.

app.component.html
<div>
  <router-outlet></router-outlet>
</div>

Made following changes in app.module.ts to import modules, services and components. We don’t want duplicate toastr messages, want messages to be displayed in center and for 2 seconds. So, configured the ToastrModule for necessary changes.

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app.routing.module';
import { GlobalValues } from './app.global';
import { AuthenticationService } from './APIService';
import { CookieService } from 'ngx-cookie-service';
import { LoginComponent } from './Login/login.component'
import { ToastrModule } from 'ngx-toastr';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent
  ],
  imports: [
    BrowserModule, BrowserAnimationsModule,
    HttpClientModule, FormsModule, AppRoutingModule, ReactiveFormsModule, ToastrModule.forRoot({
      preventDuplicates: true,
      timeOut: 2000,
      positionClass: 'toast-center-center'
    })

  ],
  providers: [GlobalValues, CookieService, AuthenticationService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now, we can access login screen after running the application using http://localhost:4200/ or http://localhost:4200/login

Let’s create AuthGaurd class that will act as a guard deciding if the route on which it is applied can be activated or not. It implements canActivate method of CanActivate interface in which the localStorage is checked for currentUser key. It returns true if currentUser exists else redirect to login with return url as querystring so that once the user login he will be redirected to this return URL.

Shared\AuthGaurd.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';

@Injectable({
    providedIn: 'root'
})
export class AuthGaurd implements CanActivate {
    constructor(private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {       
        if (localStorage.getItem("currentUser")) {
            return true;
        }
        this.router.navigate(['/login'], { queryParams: { returnURL: state.url } });
        return false;
    }
}

Now, let's create HttpRequestInterceptor class that implements HttpInterceptor. It intercepts all Http request which are made through the components of module on which it is applied. Here, it is used to append Authorization header with bearer access token.

Shared\HttpRequestInterceptor.ts
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add authorization header with access token if available
        let currentUser = JSON.parse(localStorage.getItem('currentUser'));
        if (currentUser && currentUser.UserToken) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${currentUser.UserToken}`
                }
            });
        }
        return next.handle(request);
    }
}

To import AuthGaurd and HttpRequestInterceptor by single import statement using folder path, create index.ts file in Shared folder export as follows.

Shared\Index.ts
export * from './AuthGaurd';
export * from './HttpRequestInterceptor';

Now, let’s create admin section. Create admin-layout component using angular CLI
ng g component Admin\admin-layout\admin-layout --spec=false --flat=true
It contains a header bar to show logged in user name and logout button. It also contains router-outlet placeholder for route components to render.

Admin\admin-layout\admin-layout.component.html
<!-- Navigation -->
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark fixed-top">
    <div class="container">
        <a class="navbar-brand" href="index.html">
            Angular CRUD
        </a>
        <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse"
            data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false"
            aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarResponsive">
            <ul class="navbar-nav ml-auto">
                <li class="nav-item"> <a class="nav-link"><img src="assets/images/user_icon.png" height="18"
                            width="18" /><span style="color: #0094F2"> {{username}}</span></a> </li>
                <li class="nav-item">
                    <a class="nav-link cursor-pointer" (click)="logout()">Logout <img src="assets/images/sign_out.png"
                            width="15" height="15" /></a>
                </li>
            </ul>
        </div>
    </div>
</nav>

<div id="main" style="width: 90%; margin: 0 auto; padding-top: 50px">
    <div class="row">
        <router-outlet></router-outlet>
    </div>
</div>

Admin\admin-layout\admin-layout.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-admin-layout',
  templateUrl: './admin-layout.component.html',
  styleUrls: ['./admin-layout.component.css']
})
export class AdminLayoutComponent implements OnInit {

  username: string;
  constructor(private router: Router) { }
  ngOnInit() {
    if (localStorage.getItem("currentUser")) {
      //Set the username from localStorage
this.username = JSON.parse(localStorage.getItem("currentUser")).Username;
    }
  }
  //To logout user by removing currentUser key from localStorage and navigating to login screen
  logout() {
    localStorage.removeItem("currentUser");
    this.router.navigate(['/login']);
  }

}

Let’s first create category service and product service that will be required in ManageProduct components.
CategoryService contains http request for CRUD operation of Category but currently we just need getCategoryList() to get product category to populate category dropdown.

APIService\category.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { GlobalValues } from '../app.global';
import { ICategory } from '../Models/model';

@Injectable({
  providedIn: 'root'
})
export class CategoryService {

  constructor(private http: HttpClient, private global: GlobalValues) { }

  getCategoryList() {
    return this.http.get(this.global.API_URL + "Category/GetCategory").pipe(map(res => <ICategory[]>res));
  } 
  addCategory(category: ICategory) {
    return this.http.post(this.global.API_URL + "Category/AddCategory", category).pipe(map(res => res));
  }
  updateCategory(categoryID: number, category: ICategory) {
    return this.http.put(this.global.API_URL + "Category/UpdateCategory?categoryID=" + categoryID, category).pipe(map(res => res));
  }

  deleteCategory(categoryID: number) {
    return this.http.delete(this.global.API_URL + "Category/DeleteCategory?categoryID=" + categoryID).pipe((map(res => res)));
  }
}

ProductService contains http request for CRUD operation. Here, I am also uploading product image in addProduct() so, I am creating FormData which contains product detail and uploaded product image.

APIService\product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { IProduct } from '../Models/model';
import { GlobalValues } from '../app.global';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  constructor(private http: HttpClient, private global: GlobalValues) {
  }

  getProductList() {
    return this.http.get(this.global.API_URL + "Product/GetProduct").pipe(map(res => <Product[]>res));
  }

  getProductById(productID: number) {
    return this.http.get(this.global.API_URL + "Product/GetProduct?ProductID=" + productID).pipe(map(res => <Product>res));
  }


  addProduct(product: Product, imageToUpload: File) {
    let formData = new FormData();
    formData.append("product", JSON.stringify(product));
    if (imageToUpload)
      formData.append("image", imageToUpload, imageToUpload.name);
    return this.http.post(this.global.API_URL + "Product/AddProduct", formData).pipe(map(res => res));
  }

 
  deleteProduct(productID: number) {
    return this.http.delete(this.global.API_URL + "Product/DeleteProduct?productID=" + productID).pipe((map(res => res)));
  }
}

Create index.ts file in APIService folder to import all service using single import statement.

APIService\index.ts
export * from './authentication.service';
export * from './category.service';
export * from './product.service';


Now create product-list component to show list of all products using angular CLI command
npm g component Admin/ManageProduct/ProductList/product-list --spec=false --flat=true
It contains a table to display product in each row. Last column contains Edit and Delete button. At the top of grid I have added Add Product button to add new product. Using Edit button, user can see the product detail and edit it if needed. Using delete button, product can be deleted. UI is as follows.

Admin\ManageProduct\ProductList\product-list.component.html
Product List Screen

<div class="card mt-4">
  <h5 class="card-header">Products List</h5>
  <div class="card-body">      
    <div style="overflow:auto;">
      <a routerLink="/admin/add-product" class="btn btn-primary mb-2"><span class="btn-label"><i
        class="fa fa-plus" aria-hidden="true"></i></span> Add Product</a>
      <table class="table table-bordered">
        <thead>
          <tr>
            <th style="width: 5%">SLNO.</th>
            <th style="width: 20%">Product</th>
            <th style="width: 10%">Category</th>
            <th style="width: 15%">Brand</th>
            <th style="width: 20%">Summary</th>
            <th style="width: 10%">Sale Price</th>
            <th style="width: 15%">Image</th>
            <th style="width: 20%">Action</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let product of productList; let idx=index">
            <td class="text-center">{{idx+1}}</td>
            <td>{{product.ProductName}}</td>
            <td>{{product.CategoryName}}</td>
            <td>{{product.Brand}}</td>
            <td>{{product.ProductSummary}}</td>
            <td class="text-right">{{product.SalePrice}}</td>
            <td class="text-center"><img src="{{product.ImageURL}}" width="70" height="70" alt="product image" /></td>
            <td class="text-center">
              <i class="fa fa-pencil-square cursor-pointer p-10" title="Edit Product" (click)="editProduct(product)" aria-hidden="true"></i>
              <br/>
              <i class="fa fa-trash cursor-pointer p-10" title="Delete Product" (click)="deleteProduct(product)" aria-hidden="true"></i>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

Admin\ManageProduct\ProductList\product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from 'src/app/APIService';
import { Title } from '@angular/platform-browser';
import { Product } from 'src/app/Models/model';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {

  constructor(private productService: ProductService, private title: Title,
    private router: Router, private toastr:ToastrService) { }

  //To store productList obtained from http response
  productList: Product[];
  ngOnInit() {
    //Set title of page
    this.title.setTitle("Angular CRUD-Prduct List");
    //Load saved products from DB
    this.loadProduct();
  }

  loadProduct() {
    this.productService.getProductList().subscribe(products => {
      this.productList = products;
    });
  }

  editProduct(product: Product) {
    //navigate to product edit screen with product ID
    this.router.navigate(['/admin/edit-product'], { queryParams: { 'productID': product.ProductID } })
  }

  deleteProduct(product: Product) {
    if (confirm("Do you really want to delete the Product?")) {
      this.productService.deleteProduct(product.ProductID).subscribe((resp: Response) => {
        this.toastr.success('Product Deleted Successfully.', 'Success!');
        this.loadProduct();
      },
        error => {
          this.toastr.success('Error occured while deleting.', 'Error!');
        }
      );
    }
  }
}

Create add-product component. It is used for both add and edit of product.
npm g component Admin/ManageProduct/AddProduct/add-product
Here, I have used template-driven form and NgModel to bind and validate the controls.

Admin\ManageProduct\AddProduct\add-product.component.html

Screen to add new product
Add Product Screen

Screen to edit a product
Edit Product Screen

<div class="card mt-4">
  <h5 class="card-header">{{cardHeader}} <a class="float-right" routerLink="/admin/list-product"><i class="fa fa-arrow-circle-left" aria-hidden="true"></i> Back to list</a></h5>

  <div class="card-body">  
    <div>
      <form name="form" class="form-horizontal" (ngSubmit)="onSubmit(productForm)" #productForm="ngForm">
        <div class="row">
          <div class="col-md-9">
            <input type="hidden" id="productID" name="productID" [(ngModel)]="product.ProductID" />
            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productCategory">Category</label>
              <div class="col-md-4">
                <select id="productCategory" name="productCategory" class="form-control" [(ngModel)]="product.CategoryID"
                  #categoryID="ngModel">
                  <option value="0">--Select Category--</option>
                  <option *ngFor="let category of categoryList" value="{{category.CategoryID}}">{{category.CategoryName}}</option>
                </select>
                <div *ngIf="submitted && categoryID.value==0" class="invalid">
                  <div>Product category is required</div>
                </div>
              </div>

              <label class="col-md-2 control-label text-right" for="productName">Name</label>
              <div class="col-md-4">
                <input id="productName" name="productName" #productName="ngModel" placeholder="Product Name"
                  [(ngModel)]="product.ProductName" class="form-control input-md" required type="text">
                <div *ngIf="submitted && productName.errors" class="invalid">
                  <div *ngIf="productName.errors.required">Product name is required</div>
                </div>
              </div>
            </div>
           
            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productBrand">Brand</label>
              <div class="col-md-4">
                <input id="productBrand" name="productBrand" #productBrand="ngModel" placeholder="Product Brand"
                  [(ngModel)]="product.Brand" class="form-control input-md" required type="text">
                <div *ngIf="submitted && productBrand.errors" class="invalid">
                  <div *ngIf="productBrand.errors.required">Product Brand is required</div>
                </div>
              </div>

              <label class="col-md-2 control-label text-right" for="productModel">Model</label>
              <div class="col-md-4">
                <input id="productModel" name="productModel" #productModel="ngModel" placeholder="Product Model"
                  [(ngModel)]="product.ProductModel" class="form-control input-md" required type="text">
                <div *ngIf="submitted && productModel.errors" class="invalid">
                  <div *ngIf="productModel.errors.required">Product Model is required</div>
                </div>
              </div>
            </div>
            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productSummary">Summary</label>
              <div class="col-md-4">
                <textarea id="productSummary" name="productSummary" #productSummary="ngModel" rows="2" [(ngModel)]="product.ProductSummary"
                  placeholder="Product Summary" class="form-control input-md" required></textarea>
                <div *ngIf="submitted && productSummary.errors" class="invalid">
                  <div *ngIf="productSummary.errors.required">Product summary is required</div>
                </div>
              </div>

              <label class="col-md-2 control-label text-right" for="productConfiguration">Configuration</label>
              <div class="col-md-4">
                <textarea id="productConfiguration" name="productConfiguration" [(ngModel)]="product.ProductConfiguration"
                  rows="2" placeholder="Product Configuration" class="form-control"></textarea>
              </div>
            </div>
            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productFeature">Feature</label>
              <div class="col-md-4">
                <textarea id="productFeature" name="productFeature" rows="2" [(ngModel)]="product.ProductFeature"
                  placeholder="Product Feature" class="form-control"></textarea>
              </div>

              <label class="col-md-2 control-label text-right" for="productMRP">M.R.P.</label>
              <div class="col-md-4">
                <input id="productMRP" name="productMRP" #productMRP="ngModel" placeholder="MRP" [(ngModel)]="product.MRP"
                  class="form-control input-md" required type="number">
                <div *ngIf="submitted && productMRP.value==0" class="invalid">
                  <div>Product MRP is required</div>
                </div>
              </div>
            </div>
            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productSalePrice">Sale Price</label>
              <div class="col-md-4">
                <input id="productSalePrice" name="productSalePrice" #productSalePrice="ngModel" [(ngModel)]="product.SalePrice"
                  placeholder="Sale Price" class="form-control input-md" required type="number">
                <div *ngIf="submitted && productSalePrice.value==0" class="invalid">
                  <div>Product Sale Price is required</div>
                </div>
              </div>

              <label class="col-md-2 control-label text-right" for="productTaxPers">Tax %</label>
              <div class="col-md-4">
                <input id="productTaxPers" name="productTaxPers" #productTaxPers="ngModel" [(ngModel)]="product.TaxPerc"
                  placeholder="Tax %" class="form-control input-md" required type="number">
                <div *ngIf="submitted && productTaxPers.errors" class="invalid">
                  <div *ngIf="productTaxPers.errors.required">Tax % is required</div>
                </div>
              </div>
            </div>

            <div class="form-group row">
              <label class="col-md-2 control-label text-right" for="productImage">Product Image</label>
              <div class="col-md-3">
                  {{imageFileName}}
                </div>                       
              <div class="col-md-3">
                <input id="productImage" name="productImage" #productImage class="input-file" type="file" (change)="uploadFile($event)" accept="image/*">
                <div *ngIf="submitted && mode=='add' && productImage.value==''" class="invalid">
                    <div>Product Image is required</div>
                  </div>
              </div>
              <div class="col-md-4 text-right">
                  <button id="btnSubmit" type="submit" name="btnSubmit" class="btn btn-labeled btn-primary"><span class="btn-label"><i
                        class="fa fa-check" aria-hidden="true"></i></span> Save</button>
                  <button id="btnReset" type="button" name="btnReset" class="btn btn-labeled btn-primary ml-2" (click)="setNewProduct()"><span
                      class="btn-label"><i class="fa fa-refresh" aria-hidden="true"></i></span> Reset</button>
                </div>
            </div>
           
          </div>
          <div class="col-md-3">

            <h5 class="font-bold text-center col-md-12">Image</h5>
            <div>
              <img [src]="imageURL" alt="" width="100%" />
            </div>
          
          </div>
        </div>
      </form>
    </div>
  </div>
</div>


Admin\ManageProduct\AddProduct\add-product.component.ts
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { CategoryService, ProductService } from 'src/app/APIService';
import { Title } from '@angular/platform-browser';
import { GlobalValues } from 'src/app/app.global';
import { Category, Product } from 'src/app/Models/model';
import { Router, ActivatedRoute } from '@angular/router';
import { NgForm } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'

@Component({
  selector: 'app-add-product',
  templateUrl: './add-product.component.html',
  styleUrls: ['./add-product.component.css']
})
export class AddProductComponent implements OnInit {

  constructor(
    private categoryService: CategoryService,
    private productService: ProductService,
    private title: Title,
    private global: GlobalValues,
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private changeDetection: ChangeDetectorRef,
    private toastr: ToastrService
  ) {
    this.setNewProduct();
  }

  submitted: boolean = false;
  categoryList: Category[];
  product: Product;
  mode: string = 'add'

  //Header changes based on the mode (add/edit)
  cardHeader: string = 'Add Product (Enter Product Details)';

  //store uploaded filename
  imageFileName: string = '';
  //store uploaded file object
  imageFile: File;
  //store uploaded image path
  imageURL: string = "assets/images/upload.jpg";

  ngOnInit() {

    this.title.setTitle("Angular CRUD-Add Prduct");
    this.loadCategory();
    //Set form for new product if in add mode else set the product detail to edit
    this.setNewProduct();
  }
  loadCategory() {
    this.categoryService.getCategoryList().subscribe(res => {
      this.categoryList = res;
    });
  }
  setNewProduct() {
    this.product = {
      ProductName: '',
      CategoryID: 0,
      Brand: '',
      ProductModel: '',
      ProductID: 0,
      ProductConfiguration: '',
      ProductSummary: '',
      ProductFeature: '',
      MRP: 0.0,
      SalePrice: 0.0,
      TaxPerc: 0.0,
      ImageURL: '',
      CategoryName: ''
    };
    if (this.activatedRoute.snapshot.queryParams["productID"]) {
      this.bindProduct();
    }
    else {
      this.imageFileName = "";
      this.imageFile = null;
      this.imageURL = "assets/images/upload.jpg";
    }
  }
  bindProduct() {
    if (this.activatedRoute.snapshot.queryParams["productID"]) {
      this.title.setTitle("Angular CRUD-Edit Prduct");
      this.cardHeader = 'Edit Product (Update Product Details)';
      this.mode = 'edit';
      let productID = this.activatedRoute.snapshot.queryParams["productID"];
      this.productService.getProductById(productID).subscribe(res => {

        this.product = res[0];
        this.imageURL = this.product.ImageURL;
        this.imageFileName = this.product.ImageURL.replace(/^.*[\\\/]/, '')
      });
    }
  }

  //capture event when a file is uploaded using fileupload control
  uploadFile(event) {
    const reader = new FileReader();
    if (event.target.files && event.target.files.length) {
      this.imageFile = event.target.files[0];
      reader.readAsDataURL(this.imageFile);

      reader.onload = (event: any) => {
        this.imageURL = event.target.result;
        // need to run ChangeDetection since file load runs outside of zone
        this.changeDetection.markForCheck();
      };
    }
  }

  onSubmit(productForm: NgForm) {

    this.submitted = true;
    if (productForm.invalid)
      return;

    this.productService.addProduct(this.product, this.imageFile).subscribe(data => {
      let msg = 'Product added Successfully.';
      if (this.mode == 'edit')
        msg = 'Product updated Successfully.';
      this.toastr.success(msg, 'Success!');
      this.setNewProduct();
      this.submitted = false;
    },
      error => {
        this.toastr.error('Error occured while saving.', 'Error!');
        this.submitted = false;
      }
    );
  }
}

Add following line in styles.css to display Add product form in full screen width
app-add-product{width: 100%;}

styles.css
@import url("../node_modules/font-awesome/css/font-awesome.min.css");
@import url("../node_modules/ngx-toastr/toastr.css");
.cursor-pointer{cursor: pointer;}
textarea{resize: none!important;}
.inactive{background-color: lightsalmon!important;}
.invalid{color:red;}
app-add-product{width: 100%;}


Now it’s time to set the admin routes. Create admin.routes.ts file in Admin folder. If you have remembered, I had created an admin path in app.routes.ts which lazy load AdminModule using LoadChildren. So, when user moved to Admin section, AdminLayoutComponent will be loaded by default. I have used AuthGaurd to activate this routes only if admin user is logged in.
Since I want URL should be created as http://localhost:4200/admin/[...] so I have used children paths here. So the routes will be

Admin\admin.routes.ts
import { Routes, Route } from '@angular/router'
import { AuthGaurd } from '../Shared';
import { ProductListComponent, AddProductComponent } from './ManageProduct';
import { AdminLayoutComponent } from './admin-layout/admin-layout.component';

export const adminRoutes: Routes = [
    {
        path: '', component: AdminLayoutComponent, canActivate: [AuthGaurd],
        children: [
            { path: '', component: ProductListComponent },
            { path: 'list-product', component: ProductListComponent },
            { path: 'edit-product', component: AddProductComponent },
            { path: 'add-product', component: AddProductComponent },
        ]
    }
];

Register adminRoutes as child routes in RouterModules as RouterModule.forChild(adminRoutes) and export the RouterModule.
Admin\admin-routing.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { adminRoutes } from './admin.routes'

@NgModule({
  imports: [
    CommonModule, RouterModule.forChild(adminRoutes)
  ],
  declarations: [],
  exports: [RouterModule]
})
export class AdminRoutingModule { }


Create admin.module.ts and Import the necessary components, modules and admin routing module in. Since all http request will need bearer access token to authenticate user so HttpRequestInterceptor is applied by specifying it in providers array.

Admin\admin.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AdminRoutingModule } from '../Admin/admin-routing.module';
import { AdminLayoutComponent } from './admin-layout/admin-layout.component';
import { HttpRequestInterceptor } from '../Shared';
import { ProductListComponent, AddProductComponent } from '../Admin/ManageProduct';

@NgModule({
  imports: [
    CommonModule,
    AdminRoutingModule,
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [AdminLayoutComponent, ProductListComponent, AddProductComponent],
  exports: [],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
  ]
})
export class AdminModule { }



If you like the post then comment. Thanks.