Angular 4 CRUD application using ASP.NET Web API

In this article, I will describe how to create an Angular 4 CRUD application using ASP.NET Web API. Database used is SQL Server. I will enhance the Visual Studio 2017 solution developed in the previous article – Upload multiple files using Angular 4, ASP.NET Web API, C#

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 text boxes 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 validationService.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);
    }
}

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 imported 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 variables, toggling the display of Loading Spinner and toggling the display of the modal popup dialog’s 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.

State 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.

12 thoughts on “Angular 4 CRUD application using ASP.NET Web API

  1. I have one of questions
    1) If you don’t make your productname a required field then your save button is still disabled. The save button still thinks the productname is required. How do we fix if one of the fields in our form is not required.

    Liked by 1 person

  2. I am getting this error
    Please help

    Server Error in ‘/’ Application.
    The view ‘Index’ or its master was not found or no view engine supports the searched locations. The following locations were searched:
    ~/Views/Home/Index.aspx
    ~/Views/Home/Index.ascx
    ~/Views/Shared/Index.aspx
    ~/Views/Shared/Index.ascx
    ~/Views/Home/Index.cshtml
    ~/Views/Home/Index.vbhtml
    ~/Views/Shared/Index.cshtml
    ~/Views/Shared/Index.vbhtml

    Liked by 1 person

    1. There is a specific index.html file, run that one. It will work(No need to look into home controller, i think its bypassing routing). Also ensure you have a database created first, to add and update the products ? all the best

      Like

  3. Hi Sudipta,

    I followed this article and its really helpful. Just wanted to request if possible you could create a fresh article on publishing angular 4/webapi project. (using VSTS and azure CI/CD pipeline)

    I didnt find any good article on this and its a bit cumbersome process with less documentation online.

    Best Regards
    Varun Maggo

    Like

    1. Dear Varun,

      Thanks for appreciating my article!

      On your request, I will surely try to write a new article on publishing Angular 4/WebApi project (using VSTS and Azure CI/CD pipeline) as I get time.

      Earlier there was option of creating CI or non CI deployment builds only. As you must be aware, VSTS now also provides Release Management pipeline so that you can feed your build definition in the Release management and use the release to deploy to various different environments like DEV, TEST, PRE-PROD, PROD etc.

      Thanks and regards,
      Sudipta Chaudhari

      Like

  4. What benefit does Angular 6 js have in simple CRUD? I created simple MVC Core which conducts basic CRUD on a table. (Create, read, update, delete). I took a model, conducted scaffolding, and placed the controller code into a repository. For what reason would I Need to introduce Angular? I read a lot of essays on the internet, for someone starting programming few months ago, trying to learn.

    Like

    1. Benefit of AngularJS is to create Single Page Applications (SPA). As you said you create a simple MVC Core app which conducts basic CRUD applications on a table for learning purposes, it’s not a SPA. You may create SPA with other technologies than AngularJS.

      Like

  5. Shouldn’t server-side validation be added on webapi in addition to client-side validation to be safe?

    Just wondering if that is possible with asp.net mvc 5 webapi and angular 4?

    Like

  6. I referred your article as i was creating new CRUD for Angular 8. I created both WebAPI and client Angular 8 projects.
    Later for UI i installed ‘PrimeNG’ latest version usg ‘npm install primeng –save’.
    All my package.json file settings got disturbed, then i eliminated it by ‘ng update @angular/cli’.
    Still i get below error as there is no ‘DataTable’ in new ‘PrimeNG’.
    I worked for 2 days on this but no success.
    Could u plz re-visit for latest Angular 8 + ‘PrimeNG’.
    Thanks in advance.

    Like

Leave a comment