Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

Introduction

Cosmos DB is gaining huge popularity day by day. Microsoft is also concentrating more on Cosmos DB to add extra features in this awesome NO SQL database. I have already written an article on Cosmos DB with Angular and ASP.NET Core and covered more basics in that article. You can refer to this C# Corner article for more details. Though I covered all the basic steps to create an Angular application with ASP.NET Core and Cosmos DB in the previous article, I will again tell all the steps to create a Master-Details Angular app with Cosmos DB in this article as well. We are using only a single collection to store all the skill set details to Cosmos DB along with master data. We will be embedding a data model to store the details entries. Hence, we can reduce the number of collections used in the application and accordingly reduce the total cost of the Cosmos DB service.

We will create an employee application with all employees' master data along with employee skill set details. Using the above approach, we have denormalized the employee record, by embedding all the information related to this employee, such as their skill sets details, into a single JSON document. In addition, because we're not confined to a fixed schema, we have the flexibility to do things like having skill set details of different shapes entirely.

Still, there are some downsides to using this approach. You can refer to this Microsoft document for more details on embedding data modeling.
 

Using Cosmos DB Emulator to store data locally and testing

We can use Cosmos DB emulator to store the data and test the application. This is a free, fully-functional database service. You can run the emulator on your Windows machine easily. Please download the Emulator MSI setup from this URL.

After the download and installation of the emulator, you can run it and create a new database and a collection for your application. Since we are creating an Employee application, we can create a collection named “Employee”. (In Azure Cosmos DB, collections are now called as containers).
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
You can see a URI and Primary key in this emulator. We will use these values later with our application to connect Cosmos DB. Click the Explorer tab and create a new database and a collection.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

We have given a name to database and collection. Partition key is very important in Cosmos DB and we must be very careful in choosing the partition key. Our data will be stored based on the partition key. We can’t change partition key once we created the collection.

Create Angular application in Visual Studio with ASP.NET Core

 
We can create a web application using ASP.NET Core and Angular template in Visual Studio. You may use Visual Studio 2017 or 2019 to create ASP.NET Core web application. Here, I am using dot net latest stable version 2.2. As of now, dot net core version 3.0 is in preview mode only.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

As I told you earlier, I have chosen the ASP.NET Core 2.2 version for my application. I chose the Angular template as well. Normally, it will take a few minutes to create our application with all .NET core dependencies. If you look at the project structure, you can see a “ClientApp” folder inside the root folder. This folder will only contain the basic files such as “angular.json”, “package.json” in the beginning. We can install all the node packages inside the “node_modules” folder by simply building the application.

Open the solution explorer and right click the project and select “Build” option.

Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

It will again take some more time to install all node packages to our project. After a few more minutes, our default application will be ready. You can run the application and check whether all the functionalities are working properly or not.

Sometimes, you may get the below error message while running the application.

Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

Don’t afraid of this message. Try to refresh the screen and this error will automatically disappear and you will see the actual home page now.

We can use an external library “font awesome” to enhance our UI experience with some awesome icons. You can simply add this library details inside “package.json” and rebuild the project. Related libraries files will be added automatically to the project.

Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
We can import the font-awesome library class file to “style.css” file inside the “src” folder.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
We can now use “font-awesome” icons in our entire application without further references.
 

Create Cosmos DB Service with Employees API Controller

Create the Web API service for our Angular application in ASP.NET core with Cosmos DB database. Since we are creating Employee application, we can create an “Employee” model class first.

Make a “Models” folder in the root and create “Employee” class inside it. You can copy the below code and paste to this class file.
 
Employee.cs
  1. using Newtonsoft.Json;  
  2. using Newtonsoft.Json.Linq;  
  3.   
  4. namespace AngularCosmosMasterDetails.Models  
  5. {  
  6.     public class Employee  
  7.     {  
  8.         [JsonProperty(PropertyName = "id")]  
  9.         public string Id { getset; }  
  10.         public string Name { getset; }  
  11.         public string Address { getset; }  
  12.         public string Gender { getset; }  
  13.         public string Company { getset; }  
  14.         public string Designation { getset; }  
  15.         public string Cityname { getset; }  
  16.         public JArray TechnologyStacks { getset; }  
  17.     }  
  18. }  

I have added all the field names required for our Cosmos DB collection. Note that I have added a “JsonProperty” attribute for “Id” property. Because, Cosmos DB automatically creates an “id” field for each record.

Also note that I have added a property “TechnologyStacks” in the last part of the class. The type of this property is “JArray”. Employee skill set details will be stored as an embedded document inside this field while saving and retrieving the information.

Install “Microsoft.Azure.DocumentDB.Core” NuGet package into the project. This will be used to connect Cosmos DB. You can choose the latest version and install.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

Create a “Data” folder and create an “IDocumentDBRepository” interface inside it. This interface contains all the method names for our Cosmos DB repository. We will implement this interface in the “DocumentDBRepository” class.

IDocumentDBRepository.cs
  1. using Microsoft.Azure.Documents;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.Linq.Expressions;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace AngularCosmosMasterDetails.Data  
  8. {  
  9.     public interface IDocumentDBRepository<T> where T : class  
  10.     {  
  11.         Task<Document> CreateItemAsync(T item, string collectionId);  
  12.         Task DeleteItemAsync(string id, string collectionId, string partitionKey);  
  13.         Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId);  
  14.         Task<IEnumerable<T>> GetItemsAsync(string collectionId);  
  15.         Task<Document> UpdateItemAsync(string id, T item, string collectionId);  
  16.     }  
  17. }  

We have added all the methods declaration of CRUD actions for Web API controller in the above interface.

We can implement this interface into the “DocumentDBRepository” class.
 
DocumentDBRepository.cs
  1. using Microsoft.Azure.Documents;  
  2. using Microsoft.Azure.Documents.Client;  
  3. using Microsoft.Azure.Documents.Linq;  
  4. using System;  
  5. using System.Collections.Generic;  
  6. using System.Linq;  
  7. using System.Linq.Expressions;  
  8. using System.Threading.Tasks;  
  9.   
  10. namespace AngularCosmosMasterDetails.Data  
  11. {  
  12.     public class DocumentDBRepository<T> : IDocumentDBRepository<T> where T : class  
  13.     {  
  14.   
  15.         private readonly string Endpoint = "https://localhost:8081/";  
  16.         private readonly string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";  
  17.         private readonly string DatabaseId = "SarathCosmosDB";  
  18.         private DocumentClient client;  
  19.   
  20.         public DocumentDBRepository()  
  21.         {  
  22.             client = new DocumentClient(new Uri(Endpoint), Key);  
  23.         }  
  24.   
  25.         public async Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId)  
  26.         {  
  27.             IDocumentQuery<T> query = client.CreateDocumentQuery<T>(  
  28.                 UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),  
  29.                 new FeedOptions { MaxItemCount = -1 })  
  30.                 .Where(predicate)  
  31.                 .AsDocumentQuery();  
  32.   
  33.             List<T> results = new List<T>();  
  34.             while (query.HasMoreResults)  
  35.             {  
  36.                 results.AddRange(await query.ExecuteNextAsync<T>());  
  37.             }  
  38.   
  39.             return results;  
  40.         }  
  41.   
  42.         public async Task<IEnumerable<T>> GetItemsAsync(string collectionId)  
  43.         {  
  44.             IDocumentQuery<T> query = client.CreateDocumentQuery<T>(  
  45.                 UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),  
  46.                 new FeedOptions { MaxItemCount = -1 })  
  47.                 .AsDocumentQuery();  
  48.   
  49.             List<T> results = new List<T>();  
  50.             while (query.HasMoreResults)  
  51.             {  
  52.                 results.AddRange(await query.ExecuteNextAsync<T>());  
  53.             }  
  54.   
  55.             return results;  
  56.         }  
  57.   
  58.         public async Task<Document> CreateItemAsync(T item, string collectionId)  
  59.         {  
  60.             return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), item);  
  61.         }  
  62.   
  63.         public async Task<Document> UpdateItemAsync(string id, T item, string collectionId)  
  64.         {  
  65.             return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id), item);  
  66.         }  
  67.   
  68.         public async Task DeleteItemAsync(string id, string collectionId, string partitionKey)  
  69.         {  
  70.             await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id),  
  71.             new RequestOptions() { PartitionKey = new PartitionKey(partitionKey) });  
  72.         }  
  73.   
  74.         private async Task CreateDatabaseIfNotExistsAsync()  
  75.         {  
  76.             try  
  77.             {  
  78.                 await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));  
  79.             }  
  80.             catch (DocumentClientException e)  
  81.             {  
  82.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound)  
  83.                 {  
  84.                     await client.CreateDatabaseAsync(new Database { Id = DatabaseId });  
  85.                 }  
  86.                 else  
  87.                 {  
  88.                     throw;  
  89.                 }  
  90.             }  
  91.         }  
  92.   
  93.         private async Task CreateCollectionIfNotExistsAsync(string collectionId)  
  94.         {  
  95.             try  
  96.             {  
  97.                 await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId));  
  98.             }  
  99.             catch (DocumentClientException e)  
  100.             {  
  101.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound)  
  102.                 {  
  103.                     await client.CreateDocumentCollectionAsync(  
  104.                         UriFactory.CreateDatabaseUri(DatabaseId),  
  105.                         new DocumentCollection { Id = collectionId },  
  106.                         new RequestOptions { OfferThroughput = 1000 });  
  107.                 }  
  108.                 else  
  109.                 {  
  110.                     throw;  
  111.                 }  
  112.             }  
  113.         }  
  114.     }  
  115. }  

We have implemented all the CRUD actions inside the above class. We can use these methods in our Web API controller.

We can create “EmployeesController” controller class now.
 
EmployeesController.cs
  1. using AngularCosmosMasterDetails.Data;  
  2. using AngularCosmosMasterDetails.Models;  
  3. using Microsoft.AspNetCore.Mvc;  
  4. using System.Collections.Generic;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace AngularCosmosMasterDetails.Controllers  
  8. {  
  9.     [Route("api/[controller]")]  
  10.     [ApiController]  
  11.     public class EmployeesController : ControllerBase  
  12.     {  
  13.         private readonly IDocumentDBRepository<Employee> Respository;  
  14.         private readonly string CollectionId;  
  15.         public EmployeesController(IDocumentDBRepository<Employee> Respository)  
  16.         {  
  17.             this.Respository = Respository;  
  18.             CollectionId = "Employee";  
  19.         }  
  20.   
  21.         [HttpGet]  
  22.         public async Task<IEnumerable<Employee>> Get()  
  23.         {  
  24.             return await Respository.GetItemsAsync(CollectionId);  
  25.         }  
  26.   
  27.         [HttpGet("{id}/{cityname}")]  
  28.         public async Task<Employee> Get(string id, string cityname)  
  29.         {  
  30.             var employees = await Respository.GetItemsAsync(d => d.Id == id && d.Cityname == cityname, CollectionId);  
  31.             Employee employee = new Employee();  
  32.             foreach (var emp in employees)  
  33.             {  
  34.                 employee = emp;  
  35.                 break;  
  36.             }  
  37.             return employee;  
  38.         }  
  39.   
  40.         [HttpPost]  
  41.         public async Task<bool> Post([FromBody]Employee employee)  
  42.         {  
  43.             try  
  44.             {  
  45.                 if (ModelState.IsValid)  
  46.                 {  
  47.                     employee.Id = null;  
  48.                     await Respository.CreateItemAsync(employee, CollectionId);  
  49.                 }  
  50.                 return true;  
  51.             }  
  52.             catch  
  53.             {  
  54.                 return false;  
  55.             }  
  56.   
  57.         }  
  58.   
  59.         [HttpPut]  
  60.         public async Task<bool> Put([FromBody]Employee employee)  
  61.         {  
  62.             try  
  63.             {  
  64.                 if (ModelState.IsValid)  
  65.                 {  
  66.                     await Respository.UpdateItemAsync(employee.Id, employee, CollectionId);  
  67.                 }  
  68.                 return true;  
  69.             }  
  70.             catch  
  71.             {  
  72.                 return false;  
  73.             }  
  74.         }  
  75.   
  76.         [HttpDelete("{id}/{cityname}")]  
  77.         public async Task<bool> Delete(string id, string cityname)  
  78.         {  
  79.             try  
  80.             {  
  81.                 await Respository.DeleteItemAsync(id, CollectionId, cityname);  
  82.                 return true;  
  83.             }  
  84.             catch  
  85.             {  
  86.                 return false;  
  87.             }  
  88.         }  
  89.     }  
  90. }  

We have implemented all the action methods inside this controller class using DocumentDBRepository class.

We can inject the dependency to DocumentDBRepository service inside Startup class using a singleton pattern.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

We have successfully created our Web API service with Cosmos DB. You may check the API with Postman or any other tool.

Create all components for Employee app in Angular

We can add related components for the Employee app in the Angular part of this project.

Before that, modify the home component HTML file inside the home folder.
 
home.component.html
  1. <div style="text-align:center;">  
  2.   <h1>Master-Details App in Angular with Cosmos DB using Embedded data modeling</h1>  
  3.   <p>Welcome to our new single-page application, built with below technologies:</p>  
  4.   <img src="../../assets/angular-asp-core-cosmos.png" style="width:700px;" />  
  5. </div>  

We have added a beautiful image as background in this file.

Now, we can modify the navigation menu as well.
 
nav-menu.component.html
  1. <header>  
  2.   <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>  
  3.     <div class="container">  
  4.       <a class="navbar-brand" [routerLink]='["/"]'>Employee Master-Details App</a>  
  5.       <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"  
  6.               [attr.aria-expanded]="isExpanded" (click)="toggle()">  
  7.         <span class="navbar-toggler-icon"></span>  
  8.       </button>  
  9.       <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>  
  10.         <ul class="navbar-nav flex-grow">  
  11.           <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'>  
  12.             <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a>  
  13.           </li>  
  14.           <li class="nav-item" [routerLinkActive]='["link-active"]'>  
  15.             <a class="nav-link text-dark" [routerLink]='["/employees"]'>Employees</a>  
  16.           </li>  
  17.         </ul>  
  18.       </div>  
  19.     </div>  
  20.   </nav>  
  21. </header>  
  22. <footer>  
  23.   <nav class="navbar navbar-light bg-white mt-5 fixed-bottom">  
  24.     <div class="navbar-expand m-auto navbar-text">  
  25.       Developed with <i class="fa fa-heart"></i> by <a href="https://codewithsarath.com" target="_blank"><b>Sarathlal Saseendran</b></a>  
  26.     </div>  
  27.   </nav>  
  28. </footer>  

This is the navigation part of the application. We have added an Employee menu in this file.

We can create a generic validator for validating employee name and employee city in the employee edit screen. Both these fields are mandatory. So, we must validate these fields.
 
Create a “shared” folder and create a “GenericValidator” class inside it.
 
generic-validator.ts
  1. import { FormGroup } from '@angular/forms';  
  2.   
  3. export class GenericValidator {  
  4.   
  5.   constructor(private validationMessages: { [key: string]: { [key: string]: string } }) {  
  6.   }  
  7.   
  8.   processMessages(container: FormGroup): { [key: string]: string } {  
  9.     const messages = {};  
  10.     for (const controlKey in container.controls) {  
  11.       if (container.controls.hasOwnProperty(controlKey)) {  
  12.         const c = container.controls[controlKey];  
  13.         if (c instanceof FormGroup) {  
  14.           const childMessages = this.processMessages(c);  
  15.           Object.assign(messages, childMessages);  
  16.         } else {  
  17.           if (this.validationMessages[controlKey]) {  
  18.             messages[controlKey] = '';  
  19.             if ((c.dirty || c.touched) && c.errors) {  
  20.               Object.keys(c.errors).map(messageKey => {  
  21.                 if (this.validationMessages[controlKey][messageKey]) {  
  22.                   messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' ';  
  23.                 }  
  24.               });  
  25.             }  
  26.           }  
  27.         }  
  28.       }  
  29.     }  
  30.     return messages;  
  31.   }  
  32. }  

We can create an “employee” interface inside “data-models” and declare all the employee properties inside this interface. We are creating all employee related components and service inside the “employees” folder.

employee.ts
  1. import { TechnologyStack } from "./technologystack";  
  2.   
  3. export interface Employee {  
  4.   id: string,  
  5.   name: string,  
  6.   address: string,  
  7.   gender: string,  
  8.   company: string,  
  9.   designation: string,  
  10.   cityname: string  
  11.   technologyStacks: TechnologyStack[];  
  12. }  

Please note, I have added a property “technologyStacks” as an array of type “TechnologyStack” inside this interface. Hence, we can create one more interface “TechnologyStack” inside “data-models” folder.

technologystack.ts
  1. export interface TechnologyStack {  
  2.   skillSet: string;  
  3.   experience: number;  
  4.   proficiency: number;  
  5. }  

We can create an employee service inside the services folder. We will create all the methods for CRUD operations inside this service. The methods in this service will be invoked from various components later.

employee-service.ts
  1. import { Injectable, Inject } from '@angular/core';  
  2. import { HttpClient, HttpHeaders } from '@angular/common/http';  
  3. import { Observable, throwError, of } from 'rxjs';  
  4. import { catchError, map } from 'rxjs/operators';  
  5. import { Employee } from '../data-models/employee';  
  6.   
  7. @Injectable()  
  8. export class EmployeeService {  
  9.   private employeesUrl = this.baseUrl + 'api/employees';  
  10.   
  11.   constructor(private http: HttpClient, @Inject('BASE_URL'private baseUrl: string) { }  
  12.   
  13.   getEmployees(): Observable<Employee[]> {  
  14.     return this.http.get<Employee[]>(this.employeesUrl)  
  15.       .pipe(  
  16.         catchError(this.handleError)  
  17.       );  
  18.   }  
  19.   
  20.   getEmployee(id: string, cityName: string): Observable<Employee> {  
  21.     if (id === '') {  
  22.       return of(this.initializeEmployee());  
  23.     }  
  24.     const url = `${this.employeesUrl}/${id}/${cityName}`;  
  25.     return this.http.get<Employee>(url)  
  26.       .pipe(  
  27.         catchError(this.handleError)  
  28.       );  
  29.   }  
  30.   
  31.   createEmployee(employee: Employee): Observable<Employee> {  
  32.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  33.     return this.http.post<Employee>(this.employeesUrl, employee, { headers: headers })  
  34.       .pipe(  
  35.         catchError(this.handleError)  
  36.       );  
  37.   }  
  38.   
  39.   deleteEmployee(id: string, cityname: string): Observable<{}> {  
  40.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  41.     const url = `${this.employeesUrl}/${id}/${cityname}`;  
  42.     return this.http.delete<Employee>(url, { headers: headers })  
  43.       .pipe(  
  44.         catchError(this.handleError)  
  45.       );  
  46.   }  
  47.   
  48.   updateEmployee(employee: Employee): Observable<Employee> {  
  49.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  50.     const url = this.employeesUrl;  
  51.     return this.http.put<Employee>(url, employee, { headers: headers })  
  52.       .pipe(  
  53.         map(() => employee),  
  54.         catchError(this.handleError)  
  55.       );  
  56.   }  
  57.   
  58.   private handleError(err) {  
  59.     let errorMessage: string;  
  60.     if (err.error instanceof ErrorEvent) {  
  61.       errorMessage = `An error occurred: ${err.error.message}`;  
  62.     } else {  
  63.       errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;  
  64.     }  
  65.     console.error(err);  
  66.     return throwError(errorMessage);  
  67.   }  
  68.   
  69.   private initializeEmployee(): Employee {  
  70.     return {  
  71.       id: null,  
  72.       name: null,  
  73.       address: null,  
  74.       gender: null,  
  75.       company: null,  
  76.       designation: null,  
  77.       cityname: null,  
  78.       technologyStacks: null  
  79.     };  
  80.   }  
  81. }  

We can create employee list component to list all entire employee data in a grid.

We will create this component under “employee-list” folder.
 
employee-list.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { Employee } from '../data-models/employee';  
  3. import { EmployeeService } from '../services/employee-service';  
  4.   
  5. @Component({  
  6.   selector: 'app-employee-list',  
  7.   templateUrl: './employee-list.component.html',  
  8.   styleUrls: ['./employee-list.component.css']  
  9. })  
  10. export class EmployeeListComponent implements OnInit {  
  11.   pageTitle = 'Employee List';  
  12.   filteredEmployees: Employee[] = [];  
  13.   employees: Employee[] = [];  
  14.   errorMessage = '';  
  15.   
  16.   _listFilter = '';  
  17.   get listFilter(): string {  
  18.     return this._listFilter;  
  19.   }  
  20.   set listFilter(value: string) {  
  21.     this._listFilter = value;  
  22.     this.filteredEmployees = this.listFilter ? this.performFilter(this.listFilter) : this.employees;  
  23.   }  
  24.   
  25.   constructor(private employeeService: EmployeeService) { }  
  26.   
  27.   performFilter(filterBy: string): Employee[] {  
  28.     filterBy = filterBy.toLocaleLowerCase();  
  29.     return this.employees.filter((employee: Employee) =>  
  30.       employee.name.toLocaleLowerCase().indexOf(filterBy) !== -1);  
  31.   }  
  32.   
  33.   ngOnInit(): void {  
  34.     this.employeeService.getEmployees().subscribe(  
  35.       employees => {  
  36.         this.employees = employees;  
  37.         this.filteredEmployees = this.employees;  
  38.       },  
  39.       error => this.errorMessage = <any>error  
  40.     );  
  41.   }  
  42.   
  43.   deleteEmployee(id: string, name: string, cityname: string): void {  
  44.     if (id === '') {  
  45.       this.onSaveComplete();  
  46.     } else {  
  47.       if (confirm(`Are you sure want to delete this Employee: ${name}?`)) {  
  48.         this.employeeService.deleteEmployee(id, cityname)  
  49.           .subscribe(  
  50.             () => this.onSaveComplete(),  
  51.             (error: any) => this.errorMessage = <any>error  
  52.           );  
  53.       }  
  54.     }  
  55.   }  
  56.   
  57.   onSaveComplete(): void {  
  58.     this.employeeService.getEmployees().subscribe(  
  59.       employees => {  
  60.         this.employees = employees;  
  61.         this.filteredEmployees = this.employees;  
  62.       },  
  63.       error => this.errorMessage = <any>error  
  64.     );  
  65.   }  
  66.   
  67. }  

employee-list.component.html

  1. <div class="card">  
  2.   <div class="card-header">  
  3.     {{pageTitle}}  
  4.   </div>  
  5.   <div class="card-body">  
  6.     <div class="row" style="margin-bottom:15px;">  
  7.       <div class="col-md-2">Filter by:</div>  
  8.       <div class="col-md-4">  
  9.         <input type="text" [(ngModel)]="listFilter" />  
  10.       </div>  
  11.       <div class="col-md-4"></div>  
  12.       <div class="col-md-2">  
  13.         <button class="btn btn-primary mr-3" [routerLink]="['/employees/0/0/edit']">  
  14.           <i class="fa fa-plus"></i> New Employee  
  15.         </button>  
  16.       </div>  
  17.     </div>  
  18.     <div class="row" *ngIf="listFilter">  
  19.       <div class="col-md-6">  
  20.         <h4>Filtered by: {{listFilter}}</h4>  
  21.       </div>  
  22.     </div>  
  23.     <div class="table-responsive">  
  24.       <table class="table mb-0" *ngIf="employees && employees.length">  
  25.         <thead>  
  26.           <tr>  
  27.             <th>Name</th>  
  28.             <th>Address</th>  
  29.             <th>Gender</th>  
  30.             <th>Company</th>  
  31.             <th>Designation</th>  
  32.             <th></th>  
  33.             <th></th>  
  34.           </tr>  
  35.         </thead>  
  36.         <tbody>  
  37.           <tr *ngFor="let employee of filteredEmployees">  
  38.             <td>  
  39.               <a [routerLink]="['/employees', employee.id,employee.cityname]">  
  40.                 {{ employee.name }}  
  41.               </a>  
  42.             </td>  
  43.             <td>{{ employee.address }}</td>  
  44.             <td>{{ employee.gender }}</td>  
  45.             <td>{{ employee.company }}</td>  
  46.             <td>{{ employee.designation}} </td>  
  47.             <td>  
  48.               <button class="btn btn-outline-primary btn-sm" [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">  
  49.                 <i class="fa fa-edit"></i> Edit  
  50.               </button>  
  51.             </td>  
  52.             <td>  
  53.               <button class="btn btn-outline-warning btn-sm" (click)="deleteEmployee(employee.id,  employee.name,employee.cityname);">  
  54.                 <i class="fa fa-trash"></i> Delete  
  55.               </button>  
  56.             </td>  
  57.           </tr>  
  58.         </tbody>  
  59.       </table>  
  60.     </div>  
  61.   </div>  
  62. </div>  
  63. <div *ngIf="errorMessage" class="alert alert-danger">  
  64.   Error: {{ errorMessage }}  
  65. </div>  

employee-list.component.css

  1. thead {  
  2.   color#337AB7;  
  3. }  

We have added TS, HTML, and CSS files for employee list component. We can create employee edit component in the same way. This component will be created under the “employee-edit” folder.

employee-edit.component.ts
  1. import { Component, OnInit, OnDestroy } from '@angular/core';  
  2. import { FormGroup, FormBuilder, Validators } from '@angular/forms';  
  3. import { Subscription } from 'rxjs';  
  4. import { ActivatedRoute, Router } from '@angular/router';  
  5. import { Employee } from '../data-models/employee';  
  6. import { EmployeeService } from '../services/employee-service';  
  7. import { GenericValidator } from 'src/app/shared/generic-validator';  
  8. import { SkillsManagementService } from '../skills-management/skills-management.service';  
  9. import { TechnologyStack } from '../data-models/technologystack';  
  10.   
  11. @Component({  
  12.   selector: 'app-employee-edit',  
  13.   templateUrl: './employee-edit.component.html',  
  14.   styleUrls: ['./employee-edit.component.css']  
  15. })  
  16. export class EmployeeEditComponent implements OnInit, OnDestroy {  
  17.   pageTitle = 'Employee Edit';  
  18.   errorMessage: string;  
  19.   employeeForm: FormGroup;  
  20.   tranMode: string;  
  21.   employee: Employee;  
  22.   sub: Subscription;  
  23.   technologyStacks: TechnologyStack[] = [];  
  24.   
  25.   displayMessage: { [key: string]: string } = {};  
  26.   private validationMessages: { [key: string]: { [key: string]: string } };  
  27.   genericValidator: GenericValidator;  
  28.   private currentSkillSet: any;  
  29.   
  30.   constructor(private fb: FormBuilder,  
  31.     private route: ActivatedRoute,  
  32.     private router: Router,  
  33.     private employeeService: EmployeeService,  
  34.     private skillsManagementService: SkillsManagementService) {  
  35.   
  36.     this.validationMessages = {  
  37.       name: {  
  38.         required: 'Employee name is required.',  
  39.         minlength: 'Employee name must be at least three characters.',  
  40.         maxlength: 'Employee name cannot exceed 50 characters.'  
  41.       },  
  42.       cityname: {  
  43.         required: 'Employee city name is required.',  
  44.       }  
  45.     };  
  46.     this.genericValidator = new GenericValidator(this.validationMessages);  
  47.   }  
  48.   
  49.   ngOnInit() {  
  50.     this.skillsManagementService.currentSkillSet.subscribe(skillSet => this.currentSkillSet = skillSet);  
  51.   
  52.     this.tranMode = "new";  
  53.     this.employeeForm = this.fb.group({  
  54.       name: ['', [Validators.required,  
  55.       Validators.minLength(3),  
  56.       Validators.maxLength(50)  
  57.       ]],  
  58.       address: '',  
  59.       cityname: ['', [Validators.required]],  
  60.       gender: '',  
  61.       company: '',  
  62.       designation: '',  
  63.     });  
  64.   
  65.     this.sub = this.route.paramMap.subscribe(  
  66.       params => {  
  67.         const id = params.get('id');  
  68.         const cityname = params.get('cityname');  
  69.         if (id == '0') {  
  70.           const employee: Employee = { id: "0", name: "", address: "", gender: "", company: "", designation: "", cityname: "", technologyStacks: [] };  
  71.           this.displayEmployee(employee);  
  72.         }  
  73.         else {  
  74.           this.getEmployee(id, cityname);  
  75.         }  
  76.       }  
  77.     );  
  78.   }  
  79.   
  80.   ngOnDestroy(): void {  
  81.     this.sub.unsubscribe();  
  82.   }  
  83.   
  84.   getEmployee(id: string, cityname: string): void {  
  85.     this.employeeService.getEmployee(id, cityname)  
  86.       .subscribe(  
  87.         (employee: Employee) => this.displayEmployee(employee),  
  88.         (error: any) => this.errorMessage = <any>error  
  89.       );  
  90.   }  
  91.   
  92.   displayEmployee(employee: Employee): void {  
  93.     if (this.employeeForm) {  
  94.       this.employeeForm.reset();  
  95.     }  
  96.     this.employee = employee;  
  97.     if (this.employee.id == '0') {  
  98.       this.pageTitle = 'Add Employee';  
  99.     } else {  
  100.       this.pageTitle = `Edit Employee: ${this.employee.name}`;  
  101.     }  
  102.     if (!employee.technologyStacks) employee.technologyStacks = [];  
  103.     this.technologyStacks = employee.technologyStacks;  
  104.     this.employeeForm.patchValue({  
  105.       name: this.employee.name,  
  106.       address: this.employee.address,  
  107.       gender: this.employee.gender,  
  108.       company: this.employee.company,  
  109.       designation: this.employee.designation,  
  110.       cityname: this.employee.cityname  
  111.     });  
  112.   }  
  113.   
  114.   deleteEmployee(): void {  
  115.     if (this.employee.id == '0') {  
  116.       this.onSaveComplete();  
  117.     } else {  
  118.       if (confirm(`Are you sure want to delete this Employee: ${this.employee.name}?`)) {  
  119.         this.employeeService.deleteEmployee(this.employee.id, this.employee.cityname)  
  120.           .subscribe(  
  121.             () => this.onSaveComplete(),  
  122.             (error: any) => this.errorMessage = <any>error  
  123.           );  
  124.       }  
  125.     }  
  126.   }  
  127.   
  128.   saveEmployee(): void {  
  129.     if (this.employeeForm.valid) {  
  130.       const p = { ...this.employee, ...this.employeeForm.value };  
  131.       p.technologyStacks = this.technologyStacks;  
  132.       if (p.id === '0') {  
  133.         this.employeeService.createEmployee(p)  
  134.           .subscribe(  
  135.             () => this.onSaveComplete(),  
  136.             (error: any) => this.errorMessage = <any>error  
  137.           );  
  138.       } else {  
  139.         this.employeeService.updateEmployee(p)  
  140.           .subscribe(  
  141.             () => this.onSaveComplete(),  
  142.             (error: any) => this.errorMessage = <any>error  
  143.           );  
  144.       }  
  145.   
  146.     } else {  
  147.       this.errorMessage = 'Please correct the validation errors.';  
  148.     }  
  149.   }  
  150.   
  151.   onSaveComplete(): void {  
  152.     this.employeeForm.reset();  
  153.     this.router.navigate(['/employees']);  
  154.   }  
  155.   
  156.   addStack() {  
  157.     this.skillsManagementService.showPopup(-1, nullthis.technologyStacks, () => { this.updateStack(-1) }, () => { })  
  158.   }  
  159.   
  160.   editStack(technologyStack: TechnologyStack, index: number) {  
  161.     this.skillsManagementService.showPopup(index, technologyStack, this.technologyStacks, () => { this.updateStack(index) }, () => { })  
  162.   }  
  163.   
  164.   updateStack(index: number) {  
  165.     if (index == -1) {  
  166.       this.technologyStacks.push(this.currentSkillSet);  
  167.     }  
  168.     else {  
  169.       this.technologyStacks[index] = this.currentSkillSet;  
  170.     }  
  171.   }  
  172.   
  173.   deleteStack(i: number) {  
  174.     var res = confirm("Are you sure you want to delete this Skill ?");  
  175.     if (!res) return;  
  176.     this.technologyStacks.splice(i, 1);  
  177.   }  
  178.   
  179. }  

employee-edit.component.html

  1. <div class="card">  
  2.   <div class="card-header">  
  3.     {{pageTitle}}  
  4.   </div>  
  5.   <div class="card-body">  
  6.     <div class="row">  
  7.       <div class="col-md-5">  
  8.         <form novalidate  
  9.               (ngSubmit)="saveEmployee()"  
  10.               [formGroup]="employeeForm">  
  11.   
  12.           <div class="form-group row mb-3">  
  13.             <label class="col-md-4 col-form-label"  
  14.                    for="employeeNameId">Name</label>  
  15.             <div class="col-md-8">  
  16.               <input class="form-control"  
  17.                      id="employeeNameId"  
  18.                      type="text"  
  19.                      placeholder="Name (required)"  
  20.                      formControlName="name"  
  21.                      [ngClass]="{'is-invalid': displayMessage.name }" />  
  22.               <span class="invalid-feedback">  
  23.                 {{displayMessage.name}}  
  24.               </span>  
  25.             </div>  
  26.           </div>  
  27.   
  28.           <div class="form-group row mb-3">  
  29.             <label class="col-md-4 col-form-label"  
  30.                    for="citynameId">City</label>  
  31.             <div class="col-md-8">  
  32.               <input class="form-control"  
  33.                      id="citynameid"  
  34.                      type="text"  
  35.                      placeholder="Cityname (required)"  
  36.                      formControlName="cityname"  
  37.                      [ngClass]="{'is-invalid': displayMessage.cityname}" />  
  38.               <span class="invalid-feedback">  
  39.                 {{displayMessage.cityname}}  
  40.               </span>  
  41.             </div>  
  42.           </div>  
  43.   
  44.           <div class="form-group row mb-3">  
  45.             <label class="col-md-4 col-form-label"  
  46.                    for="addressId">Address</label>  
  47.             <div class="col-md-8">  
  48.               <input class="form-control"  
  49.                      id="addressId"  
  50.                      type="text"  
  51.                      placeholder="Address"  
  52.                      formControlName="address" />  
  53.             </div>  
  54.           </div>  
  55.   
  56.           <div class="form-group row mb-3">  
  57.             <label class="col-md-4 col-form-label"  
  58.                    for="genderId">Gender</label>  
  59.             <div class="col-md-8">  
  60.               <select id="genderId" formControlName="gender" class="form-control">  
  61.                 <option value="" disabled selected>Select an Option</option>  
  62.                 <option value="Male">Male</option>  
  63.                 <option value="Female">Female</option>  
  64.               </select>  
  65.   
  66.             </div>  
  67.           </div>  
  68.   
  69.           <div class="form-group row mb-3">  
  70.             <label class="col-md-4 col-form-label"  
  71.                    for="companyId">Company</label>  
  72.             <div class="col-md-8">  
  73.               <input class="form-control"  
  74.                      id="companyId"  
  75.                      type="text"  
  76.                      placeholder="Company"  
  77.                      formControlName="company" />  
  78.             </div>  
  79.           </div>  
  80.   
  81.           <div class="form-group row mb-3">  
  82.             <label class="col-md-4 col-form-label"  
  83.                    for="designationId">Designation</label>  
  84.             <div class="col-md-8">  
  85.               <input class="form-control"  
  86.                      id="designationId"  
  87.                      type="text"  
  88.                      placeholder="Designation"  
  89.                      formControlName="designation" />  
  90.             </div>  
  91.           </div>  
  92.   
  93.           <div class="form-group row mb-3">  
  94.             <div class="offset-md-4 col-md-9">  
  95.               <button class="btn btn-primary mr-3"  
  96.                       style="width:80px;"  
  97.                       type="submit"  
  98.                       [title]="employeeForm.valid ? 'Save your entered data' : 'Disabled until the form data is valid'"  
  99.                       [disabled]="!employeeForm.valid">  
  100.                 Save  
  101.               </button>  
  102.               <button class="btn btn-outline-secondary mr-3"  
  103.                       style="width:80px;"  
  104.                       type="button"  
  105.                       title="Cancel your edits"  
  106.                       [routerLink]="['/employees']">  
  107.                 Cancel  
  108.               </button>  
  109.               <button class="btn btn-outline-warning mr-3" *ngIf="pageTitle != 'Add Employee'"  
  110.                       style="width:80px"  
  111.                       type="button"  
  112.                       title="Delete this product"  
  113.                       (click)="deleteEmployee()">  
  114.                 Delete  
  115.               </button>  
  116.             </div>  
  117.           </div>  
  118.         </form>  
  119.       </div>  
  120.       <div class="col-md-7">  
  121.         <div class="row employee-skillset-heading">  
  122.           Employee Skill Sets  
  123.         </div>  
  124.         <div class="row table-responsive" style="max-height:345px;">  
  125.           <table class="table mb-0">  
  126.             <thead>  
  127.               <tr>  
  128.                 <th style="vertical-align: middle">Technology</th>  
  129.                 <th style="vertical-align: middle">Experience <br />(in Months)</th>  
  130.                 <th style="vertical-align: middle">Proficiency</th>  
  131.                 <th colspan="2">  
  132.                   <button class="btn btn-primary mr-3" (click)="addStack()">  
  133.                     <i class="fa fa-plus"></i> New Stack  
  134.                   </button>  
  135.                 </th>  
  136.               </tr>  
  137.             </thead>  
  138.             <tbody>  
  139.               <tr *ngFor="let technologyStack of technologyStacks; let i = index">  
  140.                 <td>{{ technologyStack.skillSet }}</td>  
  141.                 <td>{{ technologyStack.experience }}</td>  
  142.                 <td>{{ technologyStack.proficiency }}</td>  
  143.                 <td>  
  144.                   <button class="btn btn-outline-primary btn-sm" (click)="editStack(technologyStack,i)">  
  145.                     <i class="fa fa-edit"></i> Edit  
  146.                   </button>  
  147.                 </td>  
  148.                 <td>  
  149.                   <button class="btn btn-outline-warning btn-sm" (click)="deleteStack(i);">  
  150.                     <i class="fa fa-trash"></i> Delete  
  151.                   </button>  
  152.                 </td>  
  153.               </tr>  
  154.             </tbody>  
  155.           </table>  
  156.         </div>  
  157.       </div>  
  158.     </div>  
  159.     <div class="alert alert-danger"  
  160.          *ngIf="errorMessage">  
  161.       {{errorMessage}}  
  162.     </div>  
  163.   </div>  
  164. </div>  

employee-edit.component.css

  1. .employee-skillset-heading {  
  2.   margin-left-5px;  
  3.   margin-bottom5px;  
  4.   font-weightbold;  
  5.   color: cornflowerblue;  
  6. }  

We can also create an “EmployeeEditGuard” guard which will be used to prevent the screen from closing accidently before saving the data. It will ask a confirmation before leaving the screen without saving the data.

employee-edit.guard.ts
  1. import { Injectable } from '@angular/core';  
  2. import { CanDeactivate } from '@angular/router';  
  3. import { Observable } from 'rxjs';  
  4. import { EmployeeEditComponent } from './employee-edit.component';  
  5.   
  6.   
  7. @Injectable({  
  8.   providedIn: 'root'  
  9. })  
  10. export class EmployeeEditGuard implements CanDeactivate<EmployeeEditComponent> {  
  11.   canDeactivate(component: EmployeeEditComponent): Observable<boolean> | Promise<boolean> | boolean {  
  12.     if (component.employeeForm.dirty) {  
  13.       const name = component.employeeForm.get('name').value || 'New Employee';  
  14.       return confirm(`Navigate away and lose all changes to ${name}?`);  
  15.     }  
  16.     return true;  
  17.   }  
  18. }   

In employee edit, we will use a modal popup to add or edit employee technical skills. We can create a new skills management component for that.

First, we can create a “SkillsManagementService” service class.
 
skills-management.service.ts
  1. import { Injectable } from '@angular/core';  
  2. import { Observable, Subject, BehaviorSubject } from 'rxjs';  
  3. import { TechnologyStack } from '../data-models/technologystack';  
  4.   
  5. @Injectable()  
  6. export class SkillsManagementService {  
  7.   private updatePopupSubject = new Subject<any>();  
  8.   private skillSetSource = new BehaviorSubject(null);  
  9.   updatePopup = this.updatePopupSubject.asObservable();  
  10.   currentSkillSet = this.skillSetSource.asObservable();  
  11.   
  12.   showPopup(currentIndex: number, skills: TechnologyStack, skillsArray: TechnologyStack[], popupYes: () => void, popupNo: () => void) {  
  13.     return this.setConfirmation(currentIndex, skills, skillsArray, popupYes, popupNo);  
  14.   }  
  15.   
  16.   setConfirmation(currentIndex: number, skills: TechnologyStack, skillsArray: TechnologyStack[], popupYes: () => void, popupNo: () => void) {  
  17.     this.updatePopupSubject.next({  
  18.       type: 'popup',  
  19.       currentIndex: currentIndex,  
  20.       skills: skills,  
  21.       skillsArray: skillsArray,  
  22.       popupYes:  
  23.         () => {  
  24.           this.updatePopupSubject.next();  
  25.           popupYes();  
  26.         },  
  27.       popupNo: () => {  
  28.         this.updatePopupSubject.next();  
  29.         popupNo();  
  30.       }  
  31.     });  
  32.   }  
  33.   
  34.   getPopupValues(): Observable<any> {  
  35.     return this.updatePopup;  
  36.   }  
  37.   
  38.   changeSkillSet(skillSet: TechnologyStack) {  
  39.     this.skillSetSource.next(skillSet);  
  40.   }  
  41.   
  42. }  

We have added a “BehaviorSubject” and “Subject” variable to control the popup movement and saving and getting data from employee edit component.

We can create the component elements now.
 
skills-management.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { FormGroup, FormBuilder } from '@angular/forms';  
  3. import { SkillsManagementService } from './skills-management.service';  
  4.   
  5. @Component({  
  6.   selector: 'app-skills-management',  
  7.   templateUrl: './skills-management.component.html',  
  8.   styleUrls: ['./skills-management.component.css']  
  9. })  
  10. export class SkillsManagementComponent implements OnInit {  
  11.   public popupValues: any;  
  12.   skillSetForm: FormGroup;  
  13.   skillsArray: string[] = [];  
  14.   proficiencyArray: number[] = [];  
  15.   editMode: string;  
  16.   modalHeaderLabel: string;  
  17.   constructor(private skillsManagementService: SkillsManagementService,  
  18.     private fb: FormBuilder) { }  
  19.   
  20.   ngOnInit() {  
  21.     for (let i = 10; i > 0; i--) {  
  22.       this.proficiencyArray.push(i);  
  23.     }  
  24.     this.skillsArray.push('Angular''ASP.NET Core''ASP.NET MVC''C#.Net''Cosmos DB','SQL Server');  
  25.     this.skillsManagementService.getPopupValues().subscribe(values => {  
  26.       this.popupValues = values;  
  27.       this.editMode = 'Add';  
  28.       this.modalHeaderLabel ='Add Skill'  
  29.       if (this.popupValues && this.popupValues.skills) {  
  30.         this.editMode = 'Update';  
  31.         this.modalHeaderLabel = 'Edit Skill'  
  32.         this.skillSetForm = this.fb.group({  
  33.           skillSet: this.popupValues.skills.skillSet,  
  34.           experience: this.popupValues.skills.experience,  
  35.           proficiency: this.popupValues.skills.proficiency  
  36.         });  
  37.       }  
  38.       else {  
  39.         this.skillSetForm = this.fb.group({  
  40.           skillSet: '',  
  41.           experience: '',  
  42.           proficiency: ''  
  43.         });  
  44.       }  
  45.     });  
  46.   
  47.   }  
  48.   
  49.   popupYes() {  
  50.     const skills = this.skillSetForm.value;  
  51.     if (!skills.skillSet || !skills.proficiency) return;  
  52.     if (this.isDuplicateSkill(skills.skillSet)) {  
  53.       alert('Same Skill already added!');  
  54.       return;  
  55.     }  
  56.     this.skillsManagementService.changeSkillSet(skills);  
  57.     this.popupValues.popupYes();  
  58.   }  
  59.   
  60.   isDuplicateSkill(skillSet: string): boolean {  
  61.     if (!this.popupValues.skillsArray) return false;  
  62.     for (let i = 0; i < this.popupValues.skillsArray.length; i++) {  
  63.       if (this.popupValues.skillsArray[i].skillSet == skillSet && i != this.popupValues.currentIndex) {  
  64.         return true;   
  65.       }  
  66.     }  
  67.     return false;  
  68.   }  
  69.   
  70.   popupNo() {  
  71.     this.skillsManagementService.changeSkillSet(null);  
  72.     this.popupValues.popupNo();  
  73.   }  
  74.   
  75. }  

skills-management.component.html

  1. <div *ngIf="popupValues">  
  2.   <div class="modal" role="dialog">  
  3.     <div class="modal-dialog " role="document">  
  4.       <div class="modal-content">  
  5.         <div *ngIf="popupValues?.type == 'popup'" class="modal-body">  
  6.           <!-- header -->  
  7.           <div class="modal-header dis-blk ">  
  8.             <button type="button" class="close" data-dismiss="modal" (click)="popupNo()">  
  9.               <img src="assets/times.png" alt="">  
  10.             </button>  
  11.           </div>  
  12.           <div class="header-modal">  
  13.             <h1>{{modalHeaderLabel}}</h1>  
  14.           </div>  
  15.   
  16.           <!-- body -->  
  17.           <div class="modal-body">  
  18.             <div class="dialogue_wrap">  
  19.               <form novalidate  
  20.                     [formGroup]="skillSetForm">  
  21.                 <div class="form-group row mb-3">  
  22.                   <label class="col-md-5 col-form-label"  
  23.                          for="skillId">Skillset</label>  
  24.                   <div class="col-md-7">  
  25.                     <select id="skillId" formControlName="skillSet" class="form-control">  
  26.                       <option value="" disabled selected>Select a Skill</option>  
  27.                       <option *ngFor="let skill of skillsArray" [value]="skill">{{skill}}</option>  
  28.                     </select>  
  29.   
  30.                   </div>  
  31.                 </div>  
  32.   
  33.                 <div class="form-group row mb-3">  
  34.                   <label class="col-md-5 col-form-label"  
  35.                          for="experienceId">Experience (in Months)</label>  
  36.                   <div class="col-md-7">  
  37.                     <input class="form-control" style="padding-left: 15px"  
  38.                            id="experienceId"  
  39.                            type="text"  
  40.                            formControlName="experience" />  
  41.                   </div>  
  42.                 </div>  
  43.   
  44.                 <div class="form-group row mb3">  
  45.                   <label class="col-md-5 col-form-label"  
  46.                          for="proficiencyId">Proficiency</label>  
  47.                   <div class="col-md-7">  
  48.                     <select id="proficiencyId" formControlName="proficiency" class="form-control">  
  49.                       <option value="" disabled selected>Select Proficiency</option>  
  50.                       <option *ngFor="let proficiency of proficiencyArray" [value]="proficiency">{{proficiency}}</option>  
  51.                     </select>  
  52.                   </div>  
  53.                 </div>  
  54.               </form>  
  55.             </div>  
  56.           </div>  
  57.           <!-- footer -->  
  58.           <div class="modal-footer">  
  59.             <div class="dialogue_btnwrap">  
  60.               <a (click)="popupNo()">  
  61.                 <input type="button" class="btn select-button" value="Cancel" />  
  62.               </a>  
  63.               <a (click)="popupYes()">  
  64.                 <input type="button" class="btn select-button" value="{{editMode}}" />  
  65.               </a>  
  66.             </div>  
  67.           </div>  
  68.         </div>  
  69.       </div>  
  70.     </div>  
  71.   </div>  
  72. </div>  

skills-management.component.css

  1. .modal-dialog{  
  2.   margin-top120px;  
  3.   max-width600px;  
  4. }  
  5.   
  6. .modal{  
  7.   display:block;  
  8. }  
  9.   
  10. .modal-body{  
  11.   padding15px 30px;  
  12. }  
  13.   
  14.   
  15. .modal-header{  
  16.   padding0px 0px;  
  17.   bordernone;  
  18.   margin-bottom60px;    
  19. }  
  20.   
  21. .header-modal{  
  22.   text-aligncenter;  
  23. }  
  24.   
  25. .header-modal h1{  
  26.   font-size33px;  
  27.   font-weight300;  
  28.   font-stylenormal;  
  29.   font-stretchnormal;  
  30.   line-heightnormal;  
  31.   letter-spacingnormal;  
  32.   text-aligncenter;  
  33.   color#4a4a4a;  
  34.   margin-bottom12px;  
  35. }  
  36.   
  37. .header-modal span{  
  38.   display:block;  
  39.   font-size17px;  
  40.   font-weightnormal;  
  41.   font-stylenormal;  
  42.   font-stretchnormal;  
  43.   line-height1.29;  
  44.   letter-spacingnormal;  
  45.   color#3c455f;  
  46. }  
  47.   
  48. .modal-body label{  
  49.   font-size14px;  
  50.   font-stylenormal;  
  51.   font-stretchnormal;  
  52.   line-height1.57;  
  53.   letter-spacingnormal;  
  54.   color#333333;  
  55. }  
  56.   
  57. .modal-body input::-webkit-input-placeholder{  
  58.   font-size15px;  
  59.   font-weightnormal;  
  60.   font-styleitalic;  
  61.   font-stretchnormal;  
  62.   line-height1.6;  
  63.   letter-spacingnormal;  
  64.   color#c6c6c6;  
  65. }  
  66. .modal-body input::-moz-input-placeholder{  
  67.   font-size15px;  
  68.   font-weightnormal;  
  69.   font-styleitalic;  
  70.   font-stretchnormal;  
  71.   line-height1.6;  
  72.   letter-spacingnormal;  
  73.   color#c6c6c6;  
  74. }  
  75. .modal-body input:-ms-input-placeholder{  
  76.   font-size15px;  
  77.   font-weightnormal;  
  78.   font-styleitalic;  
  79.   font-stretchnormal;  
  80.   line-height1.6;  
  81.   letter-spacingnormal;  
  82.   color#c6c6c6;  
  83. }  
  84. .modal-body input:-moz-placeholder{  
  85.   font-size15px;  
  86.   font-weightnormal;  
  87.   font-styleitalic;  
  88.   font-stretchnormal;  
  89.   line-height1.6;  
  90.   letter-spacingnormal;  
  91.   color#c6c6c6;  
  92. }  
  93.   
  94. .modal-body input{  
  95.   width100%;  
  96.   
  97.   border-radius: 6px;  
  98.   bordersolid 1px #d7d7d7;  
  99.   background-color#ffffff;  
  100.   
  101.   padding1px 8px;  
  102.   
  103.   font-family: Calibri;  
  104.   font-size15px;  
  105.   font-weightnormal;  
  106.   font-stylenormal;  
  107.   font-stretchnormal;  
  108.   line-height1.6;  
  109.   letter-spacingnormal;  
  110.   color#5b5b5b;  
  111. }  
  112.   
  113. .modal-body input:focus{  
  114.   outlinenone;  
  115.   box-shadow: none;  
  116. }  
  117.   
  118. .modal-footer .dialogue_btnwrap a{   
  119.   display: inline-block;  
  120.   overflowhidden;  
  121.   margin-left10px;  
  122.   width116px;  
  123. }  
  124.   
  125. .modal-footer .dialogue_btnwrap a input{   
  126.   display:inline-block;  
  127.   border-radius: 6px;  
  128.   bordersolid 1px #747474;  
  129.   
  130.   font-family: Calibri;  
  131.   font-size18px;  
  132.   font-weightnormal;  
  133.   font-stylenormal;  
  134.   font-stretchnormal;  
  135.   line-heightnormal;  
  136.   letter-spacingnormal;  
  137.   text-aligncenter;  
  138.   color#747474;  
  139.   
  140.   padding8px 10px;  
  141. }  
  142.   
  143. .modal-footer {  
  144.   border-topnone;  
  145.   padding-right33px;  
  146.   padding-top0px;  
  147. }  
  148.   
  149. .modal-footer .dialogue_btnwrap a:nth-child(2) input{  
  150.   background-color#005eb8;  
  151.   color:#fff;  
  152. }   

We must add the selector of this component "app-skills-management" inside the “app.component.html” file. Otherwise the popup will not work.

app.component.html
  1. <body>  
  2.   <app-nav-menu></app-nav-menu>  
  3.   <app-skills-management></app-skills-management>  
  4.   <div class="container">  
  5.     <router-outlet></router-outlet>  
  6.   </div>  
  7. </body>  

Create an employee detail component inside “employee-detail” folder in the same way.

employee-detail.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { ActivatedRoute, Router } from '@angular/router';  
  3. import { Employee } from '../data-models/employee';  
  4. import { EmployeeService } from '../services/employee-service';  
  5.   
  6. @Component({  
  7.   selector: 'app-employee-detail',  
  8.   templateUrl: './employee-detail.component.html',  
  9.   styleUrls: ['./employee-detail.component.css']  
  10. })  
  11. export class EmployeeDetailComponent implements OnInit {  
  12.   pageTitle = 'Employee Detail';  
  13.   errorMessage = '';  
  14.   employee: Employee | undefined;  
  15.   
  16.   constructor(private route: ActivatedRoute,  
  17.     private router: Router,  
  18.     private employeeService: EmployeeService) { }  
  19.   
  20.   ngOnInit() {  
  21.     const id = this.route.snapshot.paramMap.get('id');  
  22.     const cityname = this.route.snapshot.paramMap.get('cityname');  
  23.     if (id && cityname) {  
  24.       this.getEmployee(id, cityname);  
  25.     }  
  26.   }  
  27.   
  28.   getEmployee(id: string, cityName: string) {  
  29.     this.employeeService.getEmployee(id, cityName).subscribe(  
  30.       employee => this.employee = employee,  
  31.       error => this.errorMessage = <any>error);  
  32.   }  
  33.   
  34.   onBack(): void {  
  35.     this.router.navigate(['/employees']);  
  36.   }  
  37. }  

employee-detail.component.html

  1. <div class="card">  
  2.   <div class="card-header"  
  3.        *ngIf="employee">  
  4.     {{pageTitle + ": " + employee.name}}  
  5.   </div>  
  6.   <div class="card-body"  
  7.        *ngIf="employee">  
  8.     <div class="row">  
  9.       <div class="col-md-6">  
  10.         <div class="row">  
  11.           <div class="col-md-3">Name:</div>  
  12.           <div class="col-md-6">{{employee.name}}</div>  
  13.         </div>  
  14.         <div class="row">  
  15.           <div class="col-md-3">City:</div>  
  16.           <div class="col-md-6">{{employee.cityname}}</div>  
  17.         </div>  
  18.         <div class="row">  
  19.           <div class="col-md-3">Address:</div>  
  20.           <div class="col-md-6">{{employee.address}}</div>  
  21.         </div>  
  22.         <div class="row">  
  23.           <div class="col-md-3">Gender:</div>  
  24.           <div class="col-md-6">{{employee.gender}}</div>  
  25.         </div>  
  26.         <div class="row">  
  27.           <div class="col-md-3">Company:</div>  
  28.           <div class="col-md-6">{{employee.company}}</div>  
  29.         </div>  
  30.         <div class="row">  
  31.           <div class="col-md-3">Designation:</div>  
  32.           <div class="col-md-6">{{employee.designation}}</div>  
  33.         </div>  
  34.       </div>  
  35.       <div class="col-md-6">  
  36.         <div class="row employee-skillset-heading">  
  37.           Employee Skill Sets  
  38.         </div>  
  39.         <div class="row table-responsive" style="max-height:200px;">  
  40.           <table class="table mb-0">  
  41.             <thead>  
  42.               <tr>  
  43.                 <th style="vertical-align: middle">Technology</th>  
  44.                 <th style="vertical-align: middle">Experience (in Months)</th>  
  45.                 <th style="vertical-align: middle">Proficiency</th>  
  46.               </tr>  
  47.             </thead>  
  48.             <tbody>  
  49.               <tr *ngFor="let technologyStack of  employee.technologyStacks">  
  50.                 <td>{{ technologyStack.skillSet }}</td>  
  51.                 <td>{{ technologyStack.experience }}</td>  
  52.                 <td>{{ technologyStack.proficiency }}</td>  
  53.               </tr>  
  54.             </tbody>  
  55.           </table>  
  56.         </div>  
  57.       </div>  
  58.     </div>  
  59.     <div class="row mt-4">  
  60.       <div class="col-md-4">  
  61.         <button class="btn btn-outline-secondary mr-3"  
  62.                 style="width:80px"  
  63.                 (click)="onBack()">  
  64.           <i class="fa fa-chevron-left"></i> Back  
  65.         </button>  
  66.         <button class="btn btn-outline-primary"  
  67.                 style="width:80px"  
  68.                 [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">  
  69.           Edit  
  70.         </button>  
  71.       </div>  
  72.     </div>  
  73.   </div>  
  74.   <div class="alert alert-danger"  
  75.        *ngIf="errorMessage">  
  76.     {{errorMessage}}  
  77.   </div>  
  78. </div>  

employee-detail.component.css

  1. .employee-skillset-heading {  
  2.   margin-left-5px;  
  3.   margin-bottom5px;  
  4.   font-weightbold;  
  5.   color: cornflowerblue;  
  6. }  

Modify the “app.module.ts” file. We must add all the declarations for components and providers for services inside this file. We can also add the routing details into this file.

app.module.ts
  1. import { BrowserModule } from '@angular/platform-browser';  
  2. import { NgModule } from '@angular/core';  
  3. import { FormsModule, ReactiveFormsModule } from '@angular/forms';  
  4. import { HttpClientModule } from '@angular/common/http';  
  5. import { RouterModule } from '@angular/router';  
  6.   
  7. import { AppComponent } from './app.component';  
  8. import { NavMenuComponent } from './nav-menu/nav-menu.component';  
  9. import { HomeComponent } from './home/home.component';  
  10. import { EmployeeListComponent } from './employees/employee-list/employee-list.component';  
  11. import { EmployeeDetailComponent } from './employees/employee-detail/employee-detail.component';  
  12. import { EmployeeEditComponent } from './employees/employee-edit/employee-edit.component';  
  13. import { EmployeeEditGuard } from './employees/employee-edit/employee-edit.guard';  
  14. import { SkillsManagementComponent } from './employees/skills-management/skills-management.component';  
  15.   
  16. import { EmployeeService } from './employees/services/employee-service';  
  17. import { SkillsManagementService } from './employees/skills-management/skills-management.service';  
  18.   
  19. @NgModule({  
  20.   declarations: [  
  21.     AppComponent,  
  22.     NavMenuComponent,  
  23.     HomeComponent,  
  24.     EmployeeListComponent,  
  25.     EmployeeDetailComponent,  
  26.     EmployeeEditComponent,  
  27.     SkillsManagementComponent  
  28.   ],  
  29.   imports: [  
  30.     BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),  
  31.     HttpClientModule,  
  32.     FormsModule,  
  33.     ReactiveFormsModule,  
  34.     RouterModule.forRoot([  
  35.       { path: '', component: HomeComponent, pathMatch: 'full' },  
  36.       {  
  37.         path: 'employees',  
  38.         component: EmployeeListComponent  
  39.       },  
  40.       {  
  41.         path: 'employees/:id/:cityname',  
  42.         component: EmployeeDetailComponent  
  43.       },  
  44.       {  
  45.         path: 'employees/:id/:cityname/edit',  
  46.         canDeactivate: [EmployeeEditGuard],  
  47.         component: EmployeeEditComponent  
  48.       },  
  49.     ])  
  50.   ],  
  51.   providers: [  
  52.     EmployeeService,  
  53.     SkillsManagementService,  
  54.   ],  
  55.   bootstrap: [AppComponent]  
  56. })  
  57. export class AppModule { }  

We have completed the coding part of the application. We can execute the app now.

Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

We can click the “Employees” menu and create a new employee record.

Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
We can add new Technical skills by clicking “New Stack” button.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
I have added three skills for the employee.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
I have checked the duplication of Skillset too. If you add a skill which is already added to the employee, you will get the below error message. You can’t add a duplicate skill for an employee.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
Now we can check the local emulator and see the data stored in Cosmos DB as JSON format.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
We can add one more employee record and see the data in the grid. You can even search the employee name in Filter textbox.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
Click the employee name and see the employee detail in read-only mode.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
You can also click the Delete button to remove the employee record. It will ask for a confirmation before deleting the data.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling
 
We have seen all the CRUD actions (except update; update is also implemented in the application) with this simple employee app.
 
Master-Details App In Angular With Cosmos DB Using Embedded Data Modeling

Conclusion

We have created a web application using ASP.NET Core and Angular template in Visual Studio. We have then created a new database and collection in a Cosmos DB local emulator. We have added all components and services for an employee master-details application in Angular. We have created a modal popup to add/edit employee skill details through this modal popup. We have stored the data in Cosmos DB as embedded data format. We have seen all CRUD actions with this application. Please feel free to give your valuable feedback about this article and it will help me to improve my upcoming articles.