In my previous post Upload multiple files using Angular 4, ASP.NET Web API, C#, I described how to generate QR Code with Angular 4 using ASP.NET Web API.

In this post, I will describe how to create a CRUD application using Angular 4, HTML 5, Web API, C# and SQL Server by extending the VS2017 solution developed in the previous article.

Source Code : The complete source code for the article is available at GitHub repository : https://github.com/sudipta-chaudhari/Angular4_CRUD_WebAPI_EF

Technologies Used : Angular 4, Web API, C#, Entity Framework, SQL Server, HTML 5, Bootstrap

IDE Used :  Visual Studio 2017

External Components Used : PrimeNG

Key Learnings :

(1) CRUD Operations on SQL Server database using ASP.NET  Web API, LINQ, C#, Entity Framework

(2) Angular 4 Service – DataService, Validation Service

(3) Angular 4 – REST API call

(4) Angular 4 Reactive Forms Validation

(5) Angular 4 Grid component using PrimeNG DataTable

(6) Angular 4 modal popup using PrimeNG Dialog

Application Architecture :

Angular4_CRUD_Architecture

Application Screens :

(1) Display Products (CRUD)

Angular4_CRUD_DisplayProducts

(2) Add Product (CRUD)

Angular4_CRUD_AddProduct

(3) Update Product (CRUD)

Angular4_CRUD_EditProduct

(4) Delete Product (CRUD)

Angular4_CRUD_DeleteProduct

Let’s proceed to build the application’s code.

Step 1 : Create Database and Table

Create a new SQL Server database and a table named Product with schema as below.

Database ER Diagram :

Angular4_CRUD_DB_ERDiagram

Database Table Script :

CREATE TABLE [dbo].[Product](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [varchar](20) NOT NULL,
	[Category] [varchar](20) NOT NULL,
	[Price] [decimal](10, 2) NOT NULL,
 CONSTRAINT [PK_TblProductList] PRIMARY KEY CLUSTERED
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

Step 2 : Create ADO.NET Entity Data Model

Under application root path, add a folder named “DBModel” and add an ADO.NET Entity Data Model named InventoryModel.edmx which looks as shown below.

Angular4_CRUD_EntityDataModel

Step 3 : Create ViewModel Class

Under application root path, add a folder named “Models” and add a ViewModel class named ProductJSON.cs as shown below.

namespace Angular4WebApi.Models
{
    public class ProductJSON
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

Step 4 : Create Web API Controller

Under ‘Controllers’ folder, add a new Web API controller named ProductController.cs with the code shown below.

using Angular4WebApi.DBModel;
using Angular4WebApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace Angular4WebApi.Controllers
{
    public class ProductController : ApiController
    {
        private readonly InventoryEntities _context;

        public ProductController()
        {
            _context = new InventoryEntities();
        }
        // GET api/<controller>
        [Route("api/Product/GetProducts")]
        public IEnumerable<ProductJSON> GetProducts()
        {
            IQueryable<ProductJSON> products = _context.Product.Select(
                    p => new ProductJSON
                    {
                        Id = p.Id,
                        Name = p.Name,
                        Category = p.Category,
                        Price = p.Price
                    });
            return products.ToList();
        }
        // POST api/<controller>
        public Product Post([FromBody]Product product)
        {
            if (product == null)
            {
                throw new ArgumentNullException("product");
            }

            Product newProduct = new Product();

            try
            {
                newProduct.Name = product.Name;
                newProduct.Category = product.Category;
                newProduct.Price = product.Price;
                _context.Product.Add(newProduct);
                int rowsAffected = _context.SaveChanges();

                return rowsAffected > 0 ? product : null;
            }
            catch (Exception e)
            {
                throw e;
            }
        }
        // PUT api/<controller>/5
        public bool Put(int id, [FromBody]Product p)
        {
            p.Id = id;

            if (p == null)
            {
                throw new ArgumentNullException("p");
            }

            using (var ctx = new InventoryEntities())
            {
                var product = _context.Product.Single(a => a.Id == p.Id);

                if (product != null)
                {
                    product.Name = p.Name;
                    product.Category = p.Category;
                    product.Price = p.Price;

                    int rowsAffected = _context.SaveChanges();

                    return rowsAffected > 0 ? true : false;
                }
                else
                {
                    return false;
                }
            }
        }
        // DELETE api/<controller>/5
        public bool Delete(int id)
        {
            using (var ctx = new InventoryEntities())
            {
                Product products = ctx.Product.Find(id);
                ctx.Product.Remove(products);

                int rowsAffected = ctx.SaveChanges();

                return rowsAffected > 0 ? true : false;
            }
        }
    }
}

The Web API Controller contains the below methods:

IEnumerable GetProducts()GET API method which returns entire list of products as an IEnumerable type.

This method will be used to bind data to the PrimeNG DataTable grid.

Product Post([FromBody]Product product) – this method accepts a object of type Product which corresponds to the database Product table and returns a Product object.

This method will be used to add a new product.

public bool Put(int id, [FromBody]Product p) – this method accepts a object of type Product which corresponds to the database Product table from JSON body and a second parameter as the product id and returns a boolean.

This method will be used to update an existing product by ProductId and returns a boolean value to indicate success or failure status of update operation.

public bool Delete(int id) – this method accepts a ProductId returns a boolean.

This method will be used to delete an existing product by ProductId and returns a boolean value to indicate success or failure status of delete operation.

Step 5 : Create Angular4 Data Service

Under application root path’s ‘app’ folder (created as a part of the solution in my previous post), add a folder ‘service’ and create a typescript file dataService.ts’. This will be used to call the Web API REST endpoints using rxjs package Angular HTTP components.

This service will be Dependency Injected into the typescript component in Step (7).

The complete code for dataService.ts is provided below.

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Product } from '../model/product';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Injectable()
export class InventoryService {

    constructor(private http: Http) {}

    public getAllProducts() {
        return this.http.get('/api/Product/GetProducts').map((res: Response) => <Product[]>res.json())
    }

    addProduct(product) {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        let body = JSON.stringify(product);
        return this.http.post('/api/Product/', body, options).map((res: Response) => res.json());
    }

    updateProduct(product) {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        let body = JSON.stringify(product);
        return this.http.put('/api/Product/' + product.Id, body, options).map((res: Response) => res.json());
    }

    deleteProduct(product) {
        return this.http.delete('/api/Product/' + product.Id);
    }
}

Step 6 : Create Angular4 Validation Service

Under folder ‘service’ created in previous step, create a typescript file validationService.ts’. This will be used to validate the Angular HTML view’s textboxes to check empty field, minimum and maximum field length.

This service will be Dependency Injected into the typescript component in Step (7).

The complete code for dataService.ts is provided below.

import {FormControl} from '@angular/forms';

export class ValidationService {
    static nospaceValidator(control: FormControl): { [s: string]: boolean } {
        let re = / /;
        if (control.value && control.value.match(re)) {
            return { nospace: true };
        }
    }
}

The validation service method uses regular expression to check for empty string.

Step 7 : Create Angular4 Component Typescript

Under ‘app’ folder, create a folder named ‘product’ and add a typescript file named product.component.ts with the following code.

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { DataTableModule, SharedModule, ButtonModule, DialogModule } from 'primeng/primeng';//PrimeNg
import { Product } from '../model/product';
import { InventoryService } from '../service/dataService';
//For Reactive Forms Validation
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';
import { ValidationService } from '../service/ValidationService';

@Component({
    selector: 'app-product',
    templateUrl: './app/product/product.component.html',
    styleUrls: [
        "../../node_modules/primeng/resources/primeng.min.css",
        "../../node_modules/primeng/resources/themes/omega/theme.css",
    ],
    encapsulation: ViewEncapsulation.None,
    providers: [InventoryService]
})
export class ProductComponent implements OnInit {

    public products: Product[];
    public products_error: Boolean = false;
    public product = new Product();
    public isAdd: Boolean = false;
    public isEdit: Boolean = false;

    public isLoadingData: Boolean = false;

    addProductFG: FormGroup;
    editProductFG: FormGroup;

    addSuccess: boolean;
    editSuccess; boolean;

    displayAddDialog: boolean = false;
    displayEditDialog: boolean = false;

    constructor(private http: Http, private productService: InventoryService, private fb: FormBuilder) {
    }

    ngOnInit() {
        this.addProductFG = this.fb.group({
            'name': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(10)]],
            'category': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(5)]],
            'price': [null, Validators.required]
        });

        this.editProductFG = this.fb.group({
            'name': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(10)]],
            'category': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(5)]],
            'price': [null, Validators.required]
        });

        this.isAdd = true;
        this.isEdit = false;

        //Get Product List on Page Load
        this.getAllProducts();
    }

    public getAllProducts() {
        this.isLoadingData = true;

        this.productService.getAllProducts()
            .subscribe(
            data => {
                this.products = data;
            },
            error => {
                console.log(error),
                    this.isLoadingData = false;
            },
            () => {
                this.isLoadingData = false;
            });
    }
    editProduct(_product: Product) {
        //Show edit dialog
        this.displayEditDialog = true;

        this.isEdit = true;
        this.isAdd = false;

        this.product = { Id: _product.Id, Name: _product.Name, Category: _product.Category, Price: _product.Price };
    }
    updateProduct(product) {
        this.productService.updateProduct(product).subscribe(
            data => {
                // refresh the list
                this.getAllProducts();
                alert('Product Updated Successfully!');
                this.editSuccess = true;
                this.displayEditDialog = false;//Hide edit dialog after save

                this.product = new Product();
                this.isEdit = false;
                this.isAdd = true;
                return true;
            },
            error => {
                console.error("Error saving Product!");
                this.editSuccess = false;
                alert(error);
            }
        );
    }
    deleteProduct(_product: Product) {
        if (confirm("Are you sure you want to delete product named '" + _product.Name + "'?")) {
            this.productService.deleteProduct(_product).subscribe(
                data => {
                    // refresh the list
                    alert('Product Deleted Successfully!');
                    this.getAllProducts();
                    return true;
                },
                error => {
                    this.isLoadingData = false;
                    console.error("Error deleting Product!");
                    alert(error);
                },
                () => {
                    this.isLoadingData = false;
                }
            );
        }
    }
    clearData(): void {
        this.product = new Product();
        this.isEdit = false;
        this.isAdd = true;

        this.displayAddDialog = false;
        this.displayEditDialog = false;
    }
    addProduct(product: Product) {

        this.isAdd = true;
        this.isEdit = false;

        this.productService.addProduct(product).subscribe(
            data => {
                // refresh the list
                this.getAllProducts();
                alert('Product Added Successfully!');
                this.addSuccess = true;
                this.displayAddDialog = false;//Hide add dialog after save

                this.product = new Product();
                return true;
            },
            error => {
                console.error("Error saving Product!");
                this.addSuccess = false;
                alert(error);
            }
        );
    }
    addProductDialog() {
        this.displayAddDialog = true;
    }
}

@Component decorator marks this class as an Angular component. A component must belong to an NgModule in order for it to be usable by another component or application. To specify that a component is a member of an NgModule, it is added in the declarations field of that NgModule of app.module.ts file as follows :

import { ProductComponent } from './product/product.component';

The two services created in Step 5 and Step 6 are imorted into the product component as follows:

import { InventoryService } from '../service/dataService';
import { ValidationService } from '../service/ValidationService';

In the above typescript code various other components required have been imported.

Dependency Injection in the form of Constructor Injection is used to inject the dependencies – Http, InventoryService, FormBuilder

In the rest of the typescript code, CRUD methods from dataService.ts are called and other methods to clear the HTML form varilables, toggling the display of Loadding Spinner and toggling the display of the modal popup dialogs have been written.

Step 8 : Create Angular4 Typescript Component HTML

<style type="text/css">
    input.ng-invalid.ng-dirty.ng-touched {
        border: 2px solid Red;
        border-left: 5px solid #a94442;
    }

    /*valid and required show green*/
    .ng-valid[required] {
        border-left: 5px solid #42A948;
    }

    .error {
        padding: 12px;
        background-color: rgba(255, 0, 0, 0.2);
        color: red;
    }

    #spinner {
        background-color: rgba(49, 37, 37, 0.2);
        border-radius: 6px;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        position: fixed;
        content: " ";
        text-align: center;
        z-index: 9999;
        /*Center Div*/
        /* Safari, Opera, and Chrome */
        display: -webkit-box;
        -webkit-box-pack: center;
        -webkit-box-align: center;
        /* Firefox */
        display: -moz-box;
        -moz-box-pack: center;
        -moz-box-align: center;
        /* Internet Explorer 10 */
        display: -ms-flexbox;
        -ms-flex-pack: center;
        -ms-flex-align: center;
    }

    .lblTextLeft {
        display: block;
        text-align: left;
    }
</style>

<div *ngIf="isLoadingData" style="text-align:center" id="spinner">
    <img src="./Content/page-loader.gif" />
</div>

<div style="padding-top:10px">
    <p-dataTable [value]="products" [responsive]="true" [rows]="5" [paginator]="true">
        <p-column field="Id" hidden="hidden"></p-column>
        <p-column field="Name" header="Name" sortable="true" [filter]="true" filterPlaceholder="Search"></p-column>
        <p-column field="Category" header="Category" sortable="true" [filter]="true" filterPlaceholder="Search"></p-column>
        <p-column field="Price" header="Price" sortable="true" [filter]="true" filterPlaceholder="Search"></p-column>
        <p-column>
            <ng-template let-product="rowData" pTemplate="body">
                <button type="button" pButton (click)="editProduct(product)" icon="fa-pencil-square-o"></button>
                <button type="button" pButton (click)="deleteProduct(product)" icon="fa-times"></button>
            </ng-template>
        </p-column>
    </p-dataTable>
</div>

<p-dialog header="Add Product" [(visible)]="displayAddDialog" modal="true" width="450" height="400">
    <!-- Add Product :Using 'Reactive Forms' approach-->
    <form #addProductForm="ngForm" [formGroup]="addProductFG" class="form-horizontal" style="width:400px;height:400px">
        <div *ngIf="isAdd">
            <fieldset>
                <div class="form-group">
                    <label class="col-md-1 control-label lblTextLeft" for="name">Name</label>
                    <div class="col-md-12">
                        <input id="name" name="name" type="text" placeholder="Product Name" class="form-control input-md"
                               formControlName="name" [(ngModel)]="product.Name" required minLength="5" maxLength="10" />

                        <div *ngIf="addProductFG.get('name').hasError('required') && (addProductFG.get('name').dirty || addProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name is required</span>
                        </div>
                        <div *ngIf="addProductFG.get('name').touched && addProductFG.get('name').hasError('nospace')">
                            <span class="text-danger glyphicon glyphicon-alert">Name cannot contain space</span>
                        </div>
                        <div *ngIf="addProductFG.get('name').hasError('minlength') && (addProductFG.get('name').dirty || addProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name should be atleast 2 characters long</span>
                        </div>
                        <div *ngIf="addProductFG.get('name').hasError('maxlength') && (addProductFG.get('name').dirty || addProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name should be atmost 10 characters long</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label text-left lblTextLeft" for="category">Category</label>
                    <div class="col-md-12">
                        <input id="category" name="category" type="text" placeholder="Product Category" class="form-control input-md"
                               formControlName="category" required [(ngModel)]="product.Category" />

                        <div *ngIf="addProductFG.get('category').touched && addProductFG.get('category').hasError('required')">
                            <span class="text-danger glyphicon glyphicon-alert">Category is required</span>
                        </div>
                        <div *ngIf="addProductFG.get('category').touched && addProductFG.get('category').hasError('nospace')">
                            <span class="text-danger glyphicon glyphicon-alert">Category cannot contain space</span>
                        </div>
                        <div *ngIf="addProductFG.get('category').hasError('minlength') && (addProductFG.get('category').dirty || addProductFG.get('category').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Category should be atleast 2 characters long</span>
                        </div>
                        <div *ngIf="addProductFG.get('category').hasError('maxlength') && (addProductFG.get('category').dirty || addProductFG.get('category').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Category should be atmost 5 characters long</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label text-left lblTextLeft" for="price">Price</label>
                    <div class="col-md-12">
                        <input id="price" name="price" type="number" placeholder="Product Price" class="form-control input-md"
                               formControlName="price" required [(ngModel)]="product.Price" />

                        <div *ngIf="addProductFG.get('price').touched && addProductFG.get('price').hasError('required')">
                            <span class="text-danger glyphicon glyphicon-alert">Price is required</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label"></label>
                    <div class="col-md-12">
                        <button type="button" id="btnSave" name="btnSave" class="btn btn-primary" [disabled]="!addProductFG.valid"
                                (click)="addProduct(product);addProductForm.reset();">
                            <span class="glyphicon glyphicon-floppy-disk"></span>
                            Save
                        </button>
                        <button id="btnCancel" name="btnCancel" class="btn btn-warning" (click)="clearData();addProductForm.reset();">
                            <span class="glyphicon glyphicon-floppy-remove"></span>Cancel
                        </button>
                        <br />
                    </div>
                </div>
            </fieldset>
        </div>
    </form>
</p-dialog>

<button type="button" (click)="addProductDialog()" pButton icon="fa-external-link-square" label="Add Product"></button>

<p-dialog header="Edit Product" [(visible)]="displayEditDialog" modal="true" width="450" height="400">
    <!-- Edit Product :Using 'Reactive Forms' approach-->
    <form #editProductForm="ngForm" [formGroup]="editProductFG" class="form-horizontal" style="width:400px;height:400px">
        <div *ngIf="isEdit">
            <fieldset>
                <div class="form-group">
                    <label class="col-md-1 control-label lblTextLeft" for="name">Name</label>
                    <div class="col-md-12">
                        <input id="name" name="name" type="text" placeholder="Product Name" class="form-control input-md"
                               formControlName="name" [(ngModel)]="product.Name" required minLength="5" maxLength="10" />

                        <div *ngIf="editProductFG.get('name').hasError('required') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name is required</span>
                        </div>
                        <div *ngIf="editProductFG.get('name').touched && editProductFG.get('name').hasError('nospace')">
                            <span class="text-danger glyphicon glyphicon-alert">Name cannot contain space</span>
                        </div>
                        <div *ngIf="editProductFG.get('name').hasError('minlength') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name should be atleast 2 characters long</span>
                        </div>
                        <div *ngIf="editProductFG.get('name').hasError('maxlength') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Name should be atmost 10 characters long</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label text-left lblTextLeft" for="category">Category</label>
                    <div class="col-md-12">
                        <input id="category" name="category" type="text" placeholder="Product Category" class="form-control input-md"
                               formControlName="category" required [(ngModel)]="product.Category" />

                        <div *ngIf="editProductFG.get('category').touched && editProductFG.get('category').hasError('required')">
                            <span class="text-danger glyphicon glyphicon-alert">Category is required</span>
                        </div>
                        <div *ngIf="editProductFG.get('category').touched && editProductFG.get('category').hasError('nospace')">
                            <span class="text-danger glyphicon glyphicon-alert">Category cannot contain space</span>
                        </div>
                        <div *ngIf="editProductFG.get('category').hasError('minlength') && (editProductFG.get('category').dirty || editProductFG.get('category').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Category should be atleast 2 characters long</span>
                        </div>
                        <div *ngIf="editProductFG.get('category').hasError('maxlength') && (editProductFG.get('category').dirty || editProductFG.get('category').touched)">
                            <span class="text-danger glyphicon glyphicon-alert">Category should be atmost 5 characters long</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label text-left lblTextLeft" for="price">Price</label>
                    <div class="col-md-12">
                        <input id="price" name="price" type="number" placeholder="Product Price" class="form-control input-md"
                               formControlName="price" required [(ngModel)]="product.Price" />

                        <div *ngIf="editProductFG.get('price').touched && editProductFG.get('price').hasError('required')">
                            <span class="text-danger glyphicon glyphicon-alert">Price is required</span>
                        </div>
                    </div>
                </div>

                <div class="form-group">
                    <label class="col-md-1 control-label"></label>
                    <div class="col-md-12">
                        <button id="btnSave" name="btnSave" class="btn btn-primary"
                                type="submit" [disabled]="!editProductFG.valid" (click)="updateProduct(product);editProductForm.reset();">
                            <span class="glyphicon glyphicon-floppy-disk"></span>
                            Save
                        </button>
                        <button id="btnCancel" name="btnCancel" class="btn btn-warning" (click)="clearData();editProductForm.reset();">
                            <span class="glyphicon glyphicon-floppy-remove"></span>Cancel
                        </button>
                    </div>
                </div>
            </fieldset>
        </div>
    </form>
</p-dialog>

In the HTML code, the first div is displayed based on isLoadingData variable’s value from TypeScript component product.component.ts using an *ngIf structural directive.This div is used to display a loading spinner/busy indicator which blocks the page and displays a rotating spinner while some action is being performed like fetching the product list from the database.

Next div creates a PrimeNG DataTable for displaying the Products. DataTable has Paging, Sorting, Searching functionalities.

Next p-dialog tag creates a PrimeNG modal Dialog popup for adding a new Product. form tag has been used to create a new form. Data has been binded to the input fields using [(ngModel)]. Validation of the form has been performed using Reactive Form Validation to check for empty fields.

[formGroup]="addProductFG" has been defined in form tag. [formGroup]It is a class that tracks the value and validity state of a group of FormControl.

FormControlis a class that tracks the value and validation state of a form control.

The FormControl instance will track the value, user interaction, and validation status of the control and keep the view synced with the model.

When FormBuilder, i.e. fb is injected in product.component.ts using Constructor Injection FormBuilder instantiates new groups through this.fb.group(), each of those is a new FormGroup

product.component.ts defines addProductFG as below:

this.addProductFG = this.fb.group({
            'name': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(10)]],
            'category': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(5)]],
            'price': [null, Validators.required]
        });

Next a html button is used to open the Add Product modal dialog popup.

Next p-dialog tag creates a PrimeNG modal Dialog popup for editing an existing Product. As explained above for the Add Product dialog, form tag has been used to create a new form for editing Product data. The Edit Product dialog is displayed based on displayEditDialog varialble’s value in the typescript component product.component.ts.

As explained above for Add Product, editProductFG is created as below:

this.editProductFG = this.fb.group({
            'name': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(10)]],
            'category': [null, [Validators.required, ValidationService.nospaceValidator, Validators.minLength(2), Validators.maxLength(5)]],
            'price': [null, Validators.required]
        });

FormGroup elements are fetched by name and checked for hasError, dirty and touched as shown below.

<div *ngIf="editProductFG.get('name').hasError('required') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
    <span class="text-danger glyphicon glyphicon-alert">Name is required</span>
</div>
<div *ngIf="editProductFG.get('name').touched && editProductFG.get('name').hasError('nospace')">
    <span class="text-danger glyphicon glyphicon-alert">Name cannot contain space</span>
</div>
<div *ngIf="editProductFG.get('name').hasError('minlength') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
    <span class="text-danger glyphicon glyphicon-alert">Name should be atleast 2 characters long</span>
</div>
<div *ngIf="editProductFG.get('name').hasError('maxlength') && (editProductFG.get('name').dirty || editProductFG.get('name').touched)">
    <span class="text-danger glyphicon glyphicon-alert">Name should be atmost 10 characters long</span>
</div>

As form is validated by the Reactive Forms, below CSS classes display the styling for the valid and error states of the form’s input elements.

input.ng-invalid.ng-dirty.ng-touched {
        border: 2px solid Red;
        border-left: 5px solid #a94442;
    }

    /*valid and required show green*/
    .ng-valid[required] {
        border-left: 5px solid #42A948;
    }

    .error {
        padding: 12px;
        background-color: rgba(255, 0, 0, 0.2);
        color: red;
    }

Using ngModel in a form gives you more than just two-way data binding. It also tells you if the user touched the control, if the value changed, or if the value became invalid.

The NgModel directive doesn’t just track state, it updates the control with special Angular CSS classes that reflect the state. You can leverage those class names to change the appearance of the control.

Sate Class if true Class if flase
The control has been visited. ng-touched ng-untouched
The control’s value has changed. ng-dirty ng-pristine
The control’s value is valid. ng-valid ng-invalid

Hope you followed the article. If you have any comments, questions or suggestions, leave a message and I will try to respond at my earliest.