October 17, 2019

Part 2- Angular 8 CRUD Using Asp.Net Web API : Basic Expense Tracker App

Welcome to Part 2 of the Angular 8 CRUD series. Today we will implement the client side Angular app for our basic expense tracker. In Part 1 of this series we have already implemented the backed using Asp.Net Web API.

We will be using Angular CLI to create our angular project and its subsequent components/services hence make sure you have CLI installed.

Create the Angular App and Required Components/Services/Models

Once you create the project, add the following components. I am creating the components inside a folder named custom-component which is in src >> app:

  • expense-item (Modal to add/edit expense item)
  • expenses (Store our list of expenses)

Create two services inside a new shared folder which is in src >> app:

  • categories.service (To handle the expense categories )
  • expenses.service (To handle the expenses)

Create two models inside the same shared folder

  • ExpenseModel (To store a expenses)
  • CategoryModel (To store categories)
Angular 8 CRUD Example Using Asp.Net Web API - Angular Project Setup

Note: Kindly ignore the login-app component as it is not part of this tutorial

Add External Dependencies

We will be using FontAwesome for our icons, Angular Material for Dialog box, tool tips and date picker , and Ngx-Toastr to show the status. Follow the links below to add them to your project.

Note: Since every module for Angular Material is required to add independently, will we be adding them when the need arises.

Angular Services and Model

Category Model

export class CategoryModel{
    public Id: number;
    public ExpenseCategory : string;
}

Expense Model

export class ExpenseModel{
    public  Id :number;
    public  ExpenseAmount : number;
    public  Date? : Date;
    public  ExpenseCategory : number;
    public  Notes : string;
    public  CategoryName : string;
}

Category Service

import { Injectable } from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
//import {Response, RequestOptions, Headers } from '@angular/common/http';
import { Observable } from 'rxjs';  
import { CategoryModel } from './Category.Model';

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

base_url = "http://localhost:51888/";

  constructor(private http: HttpClient) { }

GetCategories() 
{
  return this.http.get(this.base_url + "/categories");
}

}

In the category service we have a single GetCategories method which will fetch all the categories from the back end. Make sure to add the imports for  HttpClientModule  from ‘@angular/common/http’ in you app.module.ts file.

Expense Service

In the expense service we will create two variables one to store an expense item while another will be an array to store all the expenses saved. Will will then have four different methods to perform the basic CRUD operation for expense. These methods will only call the respective API endpoints we created in the Part 1, with the required parameters.

import { Injectable } from '@angular/core';
import { ExpenseModel } from './Expense.Model';
import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
//import {Response, RequestOptions, Headers } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { CategoryModel } from './Category.Model';
import { filter, map, catchError } from 'rxjs/operators';


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

  base_url = "http://localhost:51888/";
  expenses : ExpenseModel[];
  expenseItem : ExpenseModel = new ExpenseModel();
  constructor(private http: HttpClient) { }

  AddNewExpense(form: ExpenseModel){
    
    return this.http.post(this.base_url + "/addExpense", form).
    subscribe((response) => {
     this.GetAllExpenses();
      console.log("submit was completed");
    }
    );

  }

  UpdateExpense(form: ExpenseModel)
  {
    return this.http.put(this.base_url + "expenses", form)
              .subscribe((response)=> {
                this.GetAllExpenses();
              })
  }

  GetAllExpenses()
  {
   this.http.get(this.base_url + "/expenses").
    subscribe((response) =>
    this.expenses = response as ExpenseModel[])
   ;
  }

  DeleteExpense(index: number)
  {
      
    this.http.delete(this.base_url  + "deleteExpense?exp=" + this.expenses[index].Id.toString()).
    subscribe((response) => {
     this.GetAllExpenses();
      console.log("submit was completed");
    }
    );
  }

}

Angular Components

expenses.component.html

We will now create the expenses component which will have a table with all our expenses. There will also be buttons to add, edit and delete expenses. All these buttons will have tool-tip added to it using the Angular Material matToolTip module.

For both adding and editing expense, we will use a material modal. Let us first refer the links below to add the required imports for Tool Tip, Date Picker and Modal.

<div class="col-md-10 offset-md-1 expensesDiv justify-content-center align-items-center">
    <table class="table table-bordered">
        <thead class="thead-light">
            <th>Description</th>
            <th>Expense Amount</th>
            <th>Date</th>
            <th>Expense Category</th>
            <th style="text-align: center;">
                <button class="btn btn-sm btn-success text-white" matTooltip="Add New Expense" (click)="addEditExpenseItem(null)">
                <i class="fa fa-plus"></i> <span class="ml-1"> </span>Add Expense
        </button>
            </th>
        </thead>
        <tbody>
            <tr *ngIf="expenseService.expenses.length ==0">
                <td class="font-italic text-center" colspan="5">
                    No expenses recorded
                </td>
            </tr>
            <tr *ngFor="let item of expenseService.expenses ; let i = index;">
                <td>{{item.Notes}}</td>
                <td>{{item.ExpenseAmount}}</td>
                <td>{{item.Date | date:"dd/mm/yyyy"}}</td>
                <td>{{item.CategoryName}}</td>
                <td style="text-align: center;">
                    <button class="btn btn-sm btn-info text-white actionButton mr-2" matTooltip="Edit Expense" (click)="addEditExpenseItem(i)">
                        <i class="fa fa-pencil"></i><span class="sm-1"></span> Edit
            </button>

                    <button class="btn btn-sm btn-danger text-white actionButton" matTooltip="Delete Expense" style=" background-color: red; border-style: none" b (click)="DeleteItem(i)">
                    <i class="fa fa-trash" aria-hidden="true"></i> <span class="sm-1"></span> Delete
                    </button>
                </td>
            </tr>

        </tbody>
    </table>

</div>

We have our main grid ready. Once we add the selector for expenses component in the app.component.ts file we will have something like this on the screen.

Expense Component Grid

expenses.component.ts

Let us now add the functional part of our expenses component. Include the required imports for MatDialog and ToastrService in the file as we will be using them.

In the ngOnInit life cycle hook we will call our service method to get all the expenses so that when the app loads we have the grid populated with all the required data.

Also Read: Create a custom Angular Pipe with Parameters.

We will also have a common button for editing and adding new expense. If we pass the optional index of the expense then we will have the edit operation else we will consider it to be the new add operation. In this function we will only create the required config object for a dialog box and use the “data” property on the config object to pass on the index in case of edit.

The code “this.dialog.open(ExpenseItemComponent,dialogConfig)” indicates that we want the dialog to open the ExpenseItemComponent with the configurations as specified in the dialogConfig object.

import { Component, OnInit } from '@angular/core';
import { ExpensesService } from 'src/app/shared/expenses.service';
import { MatDialogModule, MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { ExpenseItemComponent } from './../expense-item/expense-item.component';
import { ExpenseModel } from 'src/app/shared/Expense.Model';
import { ToastrService } from 'ngx-toastr';


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

  constructor(private expenseService : ExpensesService,
    private dialog : MatDialog,
    private toastr : ToastrService
    ) { }

  ngOnInit() {
    this.resetExpenseGrid();
    console.log(this.expenseService);
    this.expenseService.GetAllExpenses();
  }
   resetExpenseGrid()
   {
     this.expenseService.expenses = [];
   }

 
   
   addEditExpenseItem(expenseId? : number)
   {
     console.log(expenseId);
     
        const dialogConfig = new MatDialogConfig();
        dialogConfig.autoFocus = true;
        dialogConfig.disableClose = true;
        dialogConfig.width = "50%";
        dialogConfig.data = {
          expenseId 
        };
        this.dialog.open(ExpenseItemComponent,dialogConfig)
      
   }

   DeleteItem(index : number)
   {
     this.expenseService.DeleteExpense(index);
     this.toastr.success("Expense successfully deleted", "AEM", {
       timeOut:2000
     })
   }

   
}

expense-item.component.html

In this component we will design the actual form for our expenses. We will have a dropdown for our categories, two hidden inputs for category name and expense id, form input fields for notes, amount and matDatePicker field to show a calendar for setting the expense date.

<h5 class="display-4">Record New Expense</h5>
<hr>
<form #form = "ngForm" autocomplete="off" (submit) = "onSubmit(form)">

      
       
<div class="form-row">
        <div class="form-group col-md-12">
            <select name = "ExpenseCategory" 
            #ExpenseCategory = "ngModel"
            (change) = "CategorySelected($event.target)"
            [(ngModel)] = "formDataExpense.ExpenseCategory"
             class="form-control">
                <option value="0">--Select Expense Category--</option>
                <option *ngFor = "let cat of fromDataCategory" 
                 value="{{cat.Id}}"> {{cat.ExpenseCategory}}
            </option>
            </select>
        </div>

        <input type="hidden" name = "Id"
        #Id = "ngModel" [(ngModel)] = "formDataExpense.Id">

        <input type="hidden" name = "CategoryName"
        #CategoryName = "ngModel" [(ngModel)] = "formDataExpense.CategoryName">
</div>

<div class="form-row">
        <div class="form-group col-md-12">
        <input type="text"
                name = "Notes" 
                class="form-control"
                #Notes = "ngModel" [(ngModel)] = "formDataExpense.Notes"
                placeholder="Expense Notes">
                </div>
</div>

<div class="form-row">
    <div class="form-group col-md-5">
        <input type="number"
                name = "ExpenseAmount" 
                class="form-control"
                #ExpenseAmount = "ngModel" [(ngModel)] = "formDataExpense.ExpenseAmount"
                placeholder="Expense Amount">
    </div>
    
    <div class="form-group col-md-7" style="display: flex">
       
            <input                 
            type="text" [matDatepicker]="picker" placeholder="Expense Date" 
            name = "Date"
            #Date = "ngModel" [(ngModel)]= "formDataExpense.Date" readonly
            class="form-control">
           <mat-datepicker-toggle matSuffix [for]="picker" text-color="yellow"></mat-datepicker-toggle>
           <mat-datepicker #picker disabled = false></mat-datepicker>
     </div> 
</div>

<div class="form-row">
    <div class="col-md-8"></div>
    <div class="form-group col-md-4 " >
            <button type="submit" class="btn btn-primary btn-md text-white " style="margin-right: 8px"><i class="fa fa-save"><span class="ml-1"></span></i>Save</button>
            <button type="button" class="btn btn-danger btn-md float-right " (click)= "closeModel()"><i class="fa fa-close"></i><span class="ml-1"></span>Cancel</button>
    </div>
</div>
</form>

expense-item.component.ts

Here in the ngOnInit method, we will first check if the config of the dialog has the expenseId value passed in it or not. If it is there we will get that particular expense item from our service variable and then populate its details in the form fields for the edit operation. If it is not there then we will open the empty form for adding new expense.

Same approach we will follow on the click event of Save button as well. If it is an edit operation then we will call the UpdateExpense method of the expense service or else the AddNewExpense method.

import { Component, OnInit, Inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from  '@angular/material';
import { ExpenseModel } from 'src/app/shared/Expense.Model';
import { CategoryModel } from './../../shared/Category.Model';
import { CategoriesService } from 'src/app/shared/categories.service';
import { NgForm } from '@angular/forms';
import { ExpensesService } from 'src/app/shared/expenses.service';
import { from } from 'rxjs';
import { isNullOrUndefined } from 'util';
import { ToastrService } from 'ngx-toastr';

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

  formDataExpense : ExpenseModel = new ExpenseModel();
  fromDataCategory : CategoryModel[];

  constructor(
    @Inject(MAT_DIALOG_DATA) public data,
    public dialogRef : MatDialogRef<ExpenseItemComponent>,
    private catService : CategoriesService,
    private expenseService : ExpensesService,
    private toastr : ToastrService
  ) { }

  ngOnInit() {

    this.GetCategories();
    if(!isNullOrUndefined( this.data.expenseId))
    {
      console.log(this.expenseService.expenses[this.data.expenseId]);
      this.formDataExpense = Object.assign({},this.expenseService.expenses[this.data.expenseId]);
    }
     
  }
  closeModel()
  {
    this.dialogRef.close();
  }

  GetCategories()
  {
    this.catService.GetCategories()
    .subscribe((respose) => 
        this.fromDataCategory = respose as CategoryModel[]
    )
  }

  onSubmit(form:NgForm){
      console.log(form.value);

      if(!isNullOrUndefined( this.data.expenseId))
      {
          this.expenseService.UpdateExpense(form.value);
          this.toastr.info("Expense successfully updated","AEM" , {
            timeOut:2000
          });
      }
      else
      {
      this.expenseService.AddNewExpense(form.value);
      this.toastr.success("Expense successfully recorded","AEM" , {
        timeOut:2000
      });
      } 
      console.log("After Save");
      console.log(this.expenseService.expenses);
     
      this.closeModel();

  }

  CategorySelected(form)
  {
    if(form.selectedIndex == 0)
      this.formDataExpense.CategoryName = "";
    else{
      this.formDataExpense.CategoryName =this.fromDataCategory[form.selectedIndex -1].ExpenseCategory;
      console.log(this.formDataExpense.CategoryName);
    }
  }
}

app.component.html

<app-nav-bar></app-nav-bar>

<div class="container">
    <app-expenses style="background-color: white"></app-expenses>   
</div>

Note: I have not included the code for the <app-nav-bar> component in this article as it is just a simple bootstrap navigation bar with the heading and no actual clickable menu items.

app.module.ts

import { LoginServiceService } from './custom-components/login-service.service';
import { LoginModel } from './custom-components/login-model';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginAppComponent } from './custom-components/login-app/login-app.component';
import { NgForm, FormsModule, ReactiveFormsModule } from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import { NavBarComponent } from './custom-components/nav-bar/nav-bar.component';
import { HttpClientModule } from '@angular/common/http';
import { ExpensesComponent } from './custom-components/expenses/expenses.component';
import { ExpenseItemComponent } from './custom-components/expense-item/expense-item.component';
import {MatDialogModule} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatTooltipModule} from '@angular/material/tooltip';
import { 
  MatDatepickerModule,
  MatNativeDateModule,
  MatInputModule
} from '@angular/material';

import { ToastrModule } from 'ngx-toastr';
import {NgxPaginationModule} from 'ngx-pagination';;


@NgModule({
  declarations: [
    AppComponent,
    LoginAppComponent,
    NavBarComponent,
    ExpensesComponent,
    ExpenseItemComponent,
    
      ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    ReactiveFormsModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatIconModule,
    HttpClientModule ,
    MatDialogModule,
    MatFormFieldModule,
    MatNativeDateModule,
    MatInputModule,
    MatDatepickerModule,
 
    MatTooltipModule,
    ToastrModule.forRoot(),
    NgxPaginationModule
  ],
  entryComponents:[ExpenseItemComponent],
  exports:[MatDatepickerModule],
  providers: [LoginModel, LoginServiceService],
  bootstrap: [AppComponent]
})
export class AppModule { }

That’s it ! Our Angular 8 CRUD app should work now. Just start your back end API and you should be able to add/update/delete expenses.

In Part 3 of this series we will see how we can integrate Chart.js into our application to show some graphs for our expense trends.

7 thoughts on “Part 2- Angular 8 CRUD Using Asp.Net Web API : Basic Expense Tracker App

  1. Thanks for your write-up. What I want to point out is that when looking for a good on the internet electronics retail outlet, look for a site with full information on important factors such as the personal privacy statement, basic safety details, any payment methods, and other terms along with policies. Constantly take time to investigate the help and FAQ pieces to get a much better idea of what sort of shop will work, what they are able to do for you, and how you can make use of the features.

  2. I am so happy to read this. This is the type of manual that needs to be given and not the accidental misinformation that is at the other blogs. Appreciate your sharing this best doc.

  3. Thanks for sharing excellent informations. Your web site is very cool. I am impressed by the details that you have on this web site. It reveals how nicely you understand this subject. Bookmarked this website page, will come back for extra articles. You, my pal, ROCK! I found simply the info I already searched all over the place and just couldn’t come across. What a perfect web site.

Leave a Reply

Your email address will not be published. Required fields are marked *