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).
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.
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.
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.
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.
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.
We can import the font-awesome library class file to “style.css” file inside the “src” folder.
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
- using Newtonsoft.Json;
- using Newtonsoft.Json.Linq;
-
- namespace AngularCosmosMasterDetails.Models
- {
- public class Employee
- {
- [JsonProperty(PropertyName = "id")]
- public string Id { get; set; }
- public string Name { get; set; }
- public string Address { get; set; }
- public string Gender { get; set; }
- public string Company { get; set; }
- public string Designation { get; set; }
- public string Cityname { get; set; }
- public JArray TechnologyStacks { get; set; }
- }
- }
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.
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.
- using Microsoft.Azure.Documents;
- using System;
- using System.Collections.Generic;
- using System.Linq.Expressions;
- using System.Threading.Tasks;
-
- namespace AngularCosmosMasterDetails.Data
- {
- public interface IDocumentDBRepository<T> where T : class
- {
- Task<Document> CreateItemAsync(T item, string collectionId);
- Task DeleteItemAsync(string id, string collectionId, string partitionKey);
- Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId);
- Task<IEnumerable<T>> GetItemsAsync(string collectionId);
- Task<Document> UpdateItemAsync(string id, T item, string collectionId);
- }
- }
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.
- using Microsoft.Azure.Documents;
- using Microsoft.Azure.Documents.Client;
- using Microsoft.Azure.Documents.Linq;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Linq.Expressions;
- using System.Threading.Tasks;
-
- namespace AngularCosmosMasterDetails.Data
- {
- public class DocumentDBRepository<T> : IDocumentDBRepository<T> where T : class
- {
-
- private readonly string Endpoint = "https://localhost:8081/";
- private readonly string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
- private readonly string DatabaseId = "SarathCosmosDB";
- private DocumentClient client;
-
- public DocumentDBRepository()
- {
- client = new DocumentClient(new Uri(Endpoint), Key);
- }
-
- public async Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId)
- {
- IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
- UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),
- new FeedOptions { MaxItemCount = -1 })
- .Where(predicate)
- .AsDocumentQuery();
-
- List<T> results = new List<T>();
- while (query.HasMoreResults)
- {
- results.AddRange(await query.ExecuteNextAsync<T>());
- }
-
- return results;
- }
-
- public async Task<IEnumerable<T>> GetItemsAsync(string collectionId)
- {
- IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
- UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),
- new FeedOptions { MaxItemCount = -1 })
- .AsDocumentQuery();
-
- List<T> results = new List<T>();
- while (query.HasMoreResults)
- {
- results.AddRange(await query.ExecuteNextAsync<T>());
- }
-
- return results;
- }
-
- public async Task<Document> CreateItemAsync(T item, string collectionId)
- {
- return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), item);
- }
-
- public async Task<Document> UpdateItemAsync(string id, T item, string collectionId)
- {
- return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id), item);
- }
-
- public async Task DeleteItemAsync(string id, string collectionId, string partitionKey)
- {
- await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id),
- new RequestOptions() { PartitionKey = new PartitionKey(partitionKey) });
- }
-
- private async Task CreateDatabaseIfNotExistsAsync()
- {
- try
- {
- await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
- }
- catch (DocumentClientException e)
- {
- if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
- }
- else
- {
- throw;
- }
- }
- }
-
- private async Task CreateCollectionIfNotExistsAsync(string collectionId)
- {
- try
- {
- await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId));
- }
- catch (DocumentClientException e)
- {
- if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- await client.CreateDocumentCollectionAsync(
- UriFactory.CreateDatabaseUri(DatabaseId),
- new DocumentCollection { Id = collectionId },
- new RequestOptions { OfferThroughput = 1000 });
- }
- else
- {
- throw;
- }
- }
- }
- }
- }
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.
- using AngularCosmosMasterDetails.Data;
- using AngularCosmosMasterDetails.Models;
- using Microsoft.AspNetCore.Mvc;
- using System.Collections.Generic;
- using System.Threading.Tasks;
-
- namespace AngularCosmosMasterDetails.Controllers
- {
- [Route("api/[controller]")]
- [ApiController]
- public class EmployeesController : ControllerBase
- {
- private readonly IDocumentDBRepository<Employee> Respository;
- private readonly string CollectionId;
- public EmployeesController(IDocumentDBRepository<Employee> Respository)
- {
- this.Respository = Respository;
- CollectionId = "Employee";
- }
-
- [HttpGet]
- public async Task<IEnumerable<Employee>> Get()
- {
- return await Respository.GetItemsAsync(CollectionId);
- }
-
- [HttpGet("{id}/{cityname}")]
- public async Task<Employee> Get(string id, string cityname)
- {
- var employees = await Respository.GetItemsAsync(d => d.Id == id && d.Cityname == cityname, CollectionId);
- Employee employee = new Employee();
- foreach (var emp in employees)
- {
- employee = emp;
- break;
- }
- return employee;
- }
-
- [HttpPost]
- public async Task<bool> Post([FromBody]Employee employee)
- {
- try
- {
- if (ModelState.IsValid)
- {
- employee.Id = null;
- await Respository.CreateItemAsync(employee, CollectionId);
- }
- return true;
- }
- catch
- {
- return false;
- }
-
- }
-
- [HttpPut]
- public async Task<bool> Put([FromBody]Employee employee)
- {
- try
- {
- if (ModelState.IsValid)
- {
- await Respository.UpdateItemAsync(employee.Id, employee, CollectionId);
- }
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- [HttpDelete("{id}/{cityname}")]
- public async Task<bool> Delete(string id, string cityname)
- {
- try
- {
- await Respository.DeleteItemAsync(id, CollectionId, cityname);
- return true;
- }
- catch
- {
- return false;
- }
- }
- }
- }
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.
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.
- <div style="text-align:center;">
- <h1>Master-Details App in Angular with Cosmos DB using Embedded data modeling</h1>
- <p>Welcome to our new single-page application, built with below technologies:</p>
- <img src="../../assets/angular-asp-core-cosmos.png" style="width:700px;" />
- </div>
We have added a beautiful image as background in this file.
Now, we can modify the navigation menu as well.
- <header>
- <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>
- <div class="container">
- <a class="navbar-brand" [routerLink]='["/"]'>Employee Master-Details App</a>
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"
- [attr.aria-expanded]="isExpanded" (click)="toggle()">
- <span class="navbar-toggler-icon"></span>
- </button>
- <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>
- <ul class="navbar-nav flex-grow">
- <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'>
- <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a>
- </li>
- <li class="nav-item" [routerLinkActive]='["link-active"]'>
- <a class="nav-link text-dark" [routerLink]='["/employees"]'>Employees</a>
- </li>
- </ul>
- </div>
- </div>
- </nav>
- </header>
- <footer>
- <nav class="navbar navbar-light bg-white mt-5 fixed-bottom">
- <div class="navbar-expand m-auto navbar-text">
- Developed with <i class="fa fa-heart"></i> by <a href="https://codewithsarath.com" target="_blank"><b>Sarathlal Saseendran</b></a>
- </div>
- </nav>
- </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.
- import { FormGroup } from '@angular/forms';
-
- export class GenericValidator {
-
- constructor(private validationMessages: { [key: string]: { [key: string]: string } }) {
- }
-
- processMessages(container: FormGroup): { [key: string]: string } {
- const messages = {};
- for (const controlKey in container.controls) {
- if (container.controls.hasOwnProperty(controlKey)) {
- const c = container.controls[controlKey];
- if (c instanceof FormGroup) {
- const childMessages = this.processMessages(c);
- Object.assign(messages, childMessages);
- } else {
- if (this.validationMessages[controlKey]) {
- messages[controlKey] = '';
- if ((c.dirty || c.touched) && c.errors) {
- Object.keys(c.errors).map(messageKey => {
- if (this.validationMessages[controlKey][messageKey]) {
- messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' ';
- }
- });
- }
- }
- }
- }
- }
- return messages;
- }
- }
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.
- import { TechnologyStack } from "./technologystack";
-
- export interface Employee {
- id: string,
- name: string,
- address: string,
- gender: string,
- company: string,
- designation: string,
- cityname: string
- technologyStacks: TechnologyStack[];
- }
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.
- export interface TechnologyStack {
- skillSet: string;
- experience: number;
- proficiency: number;
- }
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.
- import { Injectable, Inject } from '@angular/core';
- import { HttpClient, HttpHeaders } from '@angular/common/http';
- import { Observable, throwError, of } from 'rxjs';
- import { catchError, map } from 'rxjs/operators';
- import { Employee } from '../data-models/employee';
-
- @Injectable()
- export class EmployeeService {
- private employeesUrl = this.baseUrl + 'api/employees';
-
- constructor(private http: HttpClient, @Inject('BASE_URL') private baseUrl: string) { }
-
- getEmployees(): Observable<Employee[]> {
- return this.http.get<Employee[]>(this.employeesUrl)
- .pipe(
- catchError(this.handleError)
- );
- }
-
- getEmployee(id: string, cityName: string): Observable<Employee> {
- if (id === '') {
- return of(this.initializeEmployee());
- }
- const url = `${this.employeesUrl}/${id}/${cityName}`;
- return this.http.get<Employee>(url)
- .pipe(
- catchError(this.handleError)
- );
- }
-
- createEmployee(employee: Employee): Observable<Employee> {
- const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
- return this.http.post<Employee>(this.employeesUrl, employee, { headers: headers })
- .pipe(
- catchError(this.handleError)
- );
- }
-
- deleteEmployee(id: string, cityname: string): Observable<{}> {
- const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
- const url = `${this.employeesUrl}/${id}/${cityname}`;
- return this.http.delete<Employee>(url, { headers: headers })
- .pipe(
- catchError(this.handleError)
- );
- }
-
- updateEmployee(employee: Employee): Observable<Employee> {
- const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
- const url = this.employeesUrl;
- return this.http.put<Employee>(url, employee, { headers: headers })
- .pipe(
- map(() => employee),
- catchError(this.handleError)
- );
- }
-
- private handleError(err) {
- let errorMessage: string;
- if (err.error instanceof ErrorEvent) {
- errorMessage = `An error occurred: ${err.error.message}`;
- } else {
- errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;
- }
- console.error(err);
- return throwError(errorMessage);
- }
-
- private initializeEmployee(): Employee {
- return {
- id: null,
- name: null,
- address: null,
- gender: null,
- company: null,
- designation: null,
- cityname: null,
- technologyStacks: null
- };
- }
- }
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
- import { Component, OnInit } from '@angular/core';
- import { Employee } from '../data-models/employee';
- import { EmployeeService } from '../services/employee-service';
-
- @Component({
- selector: 'app-employee-list',
- templateUrl: './employee-list.component.html',
- styleUrls: ['./employee-list.component.css']
- })
- export class EmployeeListComponent implements OnInit {
- pageTitle = 'Employee List';
- filteredEmployees: Employee[] = [];
- employees: Employee[] = [];
- errorMessage = '';
-
- _listFilter = '';
- get listFilter(): string {
- return this._listFilter;
- }
- set listFilter(value: string) {
- this._listFilter = value;
- this.filteredEmployees = this.listFilter ? this.performFilter(this.listFilter) : this.employees;
- }
-
- constructor(private employeeService: EmployeeService) { }
-
- performFilter(filterBy: string): Employee[] {
- filterBy = filterBy.toLocaleLowerCase();
- return this.employees.filter((employee: Employee) =>
- employee.name.toLocaleLowerCase().indexOf(filterBy) !== -1);
- }
-
- ngOnInit(): void {
- this.employeeService.getEmployees().subscribe(
- employees => {
- this.employees = employees;
- this.filteredEmployees = this.employees;
- },
- error => this.errorMessage = <any>error
- );
- }
-
- deleteEmployee(id: string, name: string, cityname: string): void {
- if (id === '') {
- this.onSaveComplete();
- } else {
- if (confirm(`Are you sure want to delete this Employee: ${name}?`)) {
- this.employeeService.deleteEmployee(id, cityname)
- .subscribe(
- () => this.onSaveComplete(),
- (error: any) => this.errorMessage = <any>error
- );
- }
- }
- }
-
- onSaveComplete(): void {
- this.employeeService.getEmployees().subscribe(
- employees => {
- this.employees = employees;
- this.filteredEmployees = this.employees;
- },
- error => this.errorMessage = <any>error
- );
- }
-
- }
employee-list.component.html
- <div class="card">
- <div class="card-header">
- {{pageTitle}}
- </div>
- <div class="card-body">
- <div class="row" style="margin-bottom:15px;">
- <div class="col-md-2">Filter by:</div>
- <div class="col-md-4">
- <input type="text" [(ngModel)]="listFilter" />
- </div>
- <div class="col-md-4"></div>
- <div class="col-md-2">
- <button class="btn btn-primary mr-3" [routerLink]="['/employees/0/0/edit']">
- <i class="fa fa-plus"></i> New Employee
- </button>
- </div>
- </div>
- <div class="row" *ngIf="listFilter">
- <div class="col-md-6">
- <h4>Filtered by: {{listFilter}}</h4>
- </div>
- </div>
- <div class="table-responsive">
- <table class="table mb-0" *ngIf="employees && employees.length">
- <thead>
- <tr>
- <th>Name</th>
- <th>Address</th>
- <th>Gender</th>
- <th>Company</th>
- <th>Designation</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let employee of filteredEmployees">
- <td>
- <a [routerLink]="['/employees', employee.id,employee.cityname]">
- {{ employee.name }}
- </a>
- </td>
- <td>{{ employee.address }}</td>
- <td>{{ employee.gender }}</td>
- <td>{{ employee.company }}</td>
- <td>{{ employee.designation}} </td>
- <td>
- <button class="btn btn-outline-primary btn-sm" [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">
- <i class="fa fa-edit"></i> Edit
- </button>
- </td>
- <td>
- <button class="btn btn-outline-warning btn-sm" (click)="deleteEmployee(employee.id, employee.name,employee.cityname);">
- <i class="fa fa-trash"></i> Delete
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <div *ngIf="errorMessage" class="alert alert-danger">
- Error: {{ errorMessage }}
- </div>
employee-list.component.css
- thead {
- color: #337AB7;
- }
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
- import { Component, OnInit, OnDestroy } from '@angular/core';
- import { FormGroup, FormBuilder, Validators } from '@angular/forms';
- import { Subscription } from 'rxjs';
- import { ActivatedRoute, Router } from '@angular/router';
- import { Employee } from '../data-models/employee';
- import { EmployeeService } from '../services/employee-service';
- import { GenericValidator } from 'src/app/shared/generic-validator';
- import { SkillsManagementService } from '../skills-management/skills-management.service';
- import { TechnologyStack } from '../data-models/technologystack';
-
- @Component({
- selector: 'app-employee-edit',
- templateUrl: './employee-edit.component.html',
- styleUrls: ['./employee-edit.component.css']
- })
- export class EmployeeEditComponent implements OnInit, OnDestroy {
- pageTitle = 'Employee Edit';
- errorMessage: string;
- employeeForm: FormGroup;
- tranMode: string;
- employee: Employee;
- sub: Subscription;
- technologyStacks: TechnologyStack[] = [];
-
- displayMessage: { [key: string]: string } = {};
- private validationMessages: { [key: string]: { [key: string]: string } };
- genericValidator: GenericValidator;
- private currentSkillSet: any;
-
- constructor(private fb: FormBuilder,
- private route: ActivatedRoute,
- private router: Router,
- private employeeService: EmployeeService,
- private skillsManagementService: SkillsManagementService) {
-
- this.validationMessages = {
- name: {
- required: 'Employee name is required.',
- minlength: 'Employee name must be at least three characters.',
- maxlength: 'Employee name cannot exceed 50 characters.'
- },
- cityname: {
- required: 'Employee city name is required.',
- }
- };
- this.genericValidator = new GenericValidator(this.validationMessages);
- }
-
- ngOnInit() {
- this.skillsManagementService.currentSkillSet.subscribe(skillSet => this.currentSkillSet = skillSet);
-
- this.tranMode = "new";
- this.employeeForm = this.fb.group({
- name: ['', [Validators.required,
- Validators.minLength(3),
- Validators.maxLength(50)
- ]],
- address: '',
- cityname: ['', [Validators.required]],
- gender: '',
- company: '',
- designation: '',
- });
-
- this.sub = this.route.paramMap.subscribe(
- params => {
- const id = params.get('id');
- const cityname = params.get('cityname');
- if (id == '0') {
- const employee: Employee = { id: "0", name: "", address: "", gender: "", company: "", designation: "", cityname: "", technologyStacks: [] };
- this.displayEmployee(employee);
- }
- else {
- this.getEmployee(id, cityname);
- }
- }
- );
- }
-
- ngOnDestroy(): void {
- this.sub.unsubscribe();
- }
-
- getEmployee(id: string, cityname: string): void {
- this.employeeService.getEmployee(id, cityname)
- .subscribe(
- (employee: Employee) => this.displayEmployee(employee),
- (error: any) => this.errorMessage = <any>error
- );
- }
-
- displayEmployee(employee: Employee): void {
- if (this.employeeForm) {
- this.employeeForm.reset();
- }
- this.employee = employee;
- if (this.employee.id == '0') {
- this.pageTitle = 'Add Employee';
- } else {
- this.pageTitle = `Edit Employee: ${this.employee.name}`;
- }
- if (!employee.technologyStacks) employee.technologyStacks = [];
- this.technologyStacks = employee.technologyStacks;
- this.employeeForm.patchValue({
- name: this.employee.name,
- address: this.employee.address,
- gender: this.employee.gender,
- company: this.employee.company,
- designation: this.employee.designation,
- cityname: this.employee.cityname
- });
- }
-
- deleteEmployee(): void {
- if (this.employee.id == '0') {
- this.onSaveComplete();
- } else {
- if (confirm(`Are you sure want to delete this Employee: ${this.employee.name}?`)) {
- this.employeeService.deleteEmployee(this.employee.id, this.employee.cityname)
- .subscribe(
- () => this.onSaveComplete(),
- (error: any) => this.errorMessage = <any>error
- );
- }
- }
- }
-
- saveEmployee(): void {
- if (this.employeeForm.valid) {
- const p = { ...this.employee, ...this.employeeForm.value };
- p.technologyStacks = this.technologyStacks;
- if (p.id === '0') {
- this.employeeService.createEmployee(p)
- .subscribe(
- () => this.onSaveComplete(),
- (error: any) => this.errorMessage = <any>error
- );
- } else {
- this.employeeService.updateEmployee(p)
- .subscribe(
- () => this.onSaveComplete(),
- (error: any) => this.errorMessage = <any>error
- );
- }
-
- } else {
- this.errorMessage = 'Please correct the validation errors.';
- }
- }
-
- onSaveComplete(): void {
- this.employeeForm.reset();
- this.router.navigate(['/employees']);
- }
-
- addStack() {
- this.skillsManagementService.showPopup(-1, null, this.technologyStacks, () => { this.updateStack(-1) }, () => { })
- }
-
- editStack(technologyStack: TechnologyStack, index: number) {
- this.skillsManagementService.showPopup(index, technologyStack, this.technologyStacks, () => { this.updateStack(index) }, () => { })
- }
-
- updateStack(index: number) {
- if (index == -1) {
- this.technologyStacks.push(this.currentSkillSet);
- }
- else {
- this.technologyStacks[index] = this.currentSkillSet;
- }
- }
-
- deleteStack(i: number) {
- var res = confirm("Are you sure you want to delete this Skill ?");
- if (!res) return;
- this.technologyStacks.splice(i, 1);
- }
-
- }
employee-edit.component.html
employee-edit.component.css
- .employee-skillset-heading {
- margin-left: -5px;
- margin-bottom: 5px;
- font-weight: bold;
- color: cornflowerblue;
- }
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.
- import { Injectable } from '@angular/core';
- import { CanDeactivate } from '@angular/router';
- import { Observable } from 'rxjs';
- import { EmployeeEditComponent } from './employee-edit.component';
-
-
- @Injectable({
- providedIn: 'root'
- })
- export class EmployeeEditGuard implements CanDeactivate<EmployeeEditComponent> {
- canDeactivate(component: EmployeeEditComponent): Observable<boolean> | Promise<boolean> | boolean {
- if (component.employeeForm.dirty) {
- const name = component.employeeForm.get('name').value || 'New Employee';
- return confirm(`Navigate away and lose all changes to ${name}?`);
- }
- return true;
- }
- }
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
- import { Injectable } from '@angular/core';
- import { Observable, Subject, BehaviorSubject } from 'rxjs';
- import { TechnologyStack } from '../data-models/technologystack';
-
- @Injectable()
- export class SkillsManagementService {
- private updatePopupSubject = new Subject<any>();
- private skillSetSource = new BehaviorSubject(null);
- updatePopup = this.updatePopupSubject.asObservable();
- currentSkillSet = this.skillSetSource.asObservable();
-
- showPopup(currentIndex: number, skills: TechnologyStack, skillsArray: TechnologyStack[], popupYes: () => void, popupNo: () => void) {
- return this.setConfirmation(currentIndex, skills, skillsArray, popupYes, popupNo);
- }
-
- setConfirmation(currentIndex: number, skills: TechnologyStack, skillsArray: TechnologyStack[], popupYes: () => void, popupNo: () => void) {
- this.updatePopupSubject.next({
- type: 'popup',
- currentIndex: currentIndex,
- skills: skills,
- skillsArray: skillsArray,
- popupYes:
- () => {
- this.updatePopupSubject.next();
- popupYes();
- },
- popupNo: () => {
- this.updatePopupSubject.next();
- popupNo();
- }
- });
- }
-
- getPopupValues(): Observable<any> {
- return this.updatePopup;
- }
-
- changeSkillSet(skillSet: TechnologyStack) {
- this.skillSetSource.next(skillSet);
- }
-
- }
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
- import { Component, OnInit } from '@angular/core';
- import { FormGroup, FormBuilder } from '@angular/forms';
- import { SkillsManagementService } from './skills-management.service';
-
- @Component({
- selector: 'app-skills-management',
- templateUrl: './skills-management.component.html',
- styleUrls: ['./skills-management.component.css']
- })
- export class SkillsManagementComponent implements OnInit {
- public popupValues: any;
- skillSetForm: FormGroup;
- skillsArray: string[] = [];
- proficiencyArray: number[] = [];
- editMode: string;
- modalHeaderLabel: string;
- constructor(private skillsManagementService: SkillsManagementService,
- private fb: FormBuilder) { }
-
- ngOnInit() {
- for (let i = 10; i > 0; i--) {
- this.proficiencyArray.push(i);
- }
- this.skillsArray.push('Angular', 'ASP.NET Core', 'ASP.NET MVC', 'C#.Net', 'Cosmos DB','SQL Server');
- this.skillsManagementService.getPopupValues().subscribe(values => {
- this.popupValues = values;
- this.editMode = 'Add';
- this.modalHeaderLabel ='Add Skill'
- if (this.popupValues && this.popupValues.skills) {
- this.editMode = 'Update';
- this.modalHeaderLabel = 'Edit Skill'
- this.skillSetForm = this.fb.group({
- skillSet: this.popupValues.skills.skillSet,
- experience: this.popupValues.skills.experience,
- proficiency: this.popupValues.skills.proficiency
- });
- }
- else {
- this.skillSetForm = this.fb.group({
- skillSet: '',
- experience: '',
- proficiency: ''
- });
- }
- });
-
- }
-
- popupYes() {
- const skills = this.skillSetForm.value;
- if (!skills.skillSet || !skills.proficiency) return;
- if (this.isDuplicateSkill(skills.skillSet)) {
- alert('Same Skill already added!');
- return;
- }
- this.skillsManagementService.changeSkillSet(skills);
- this.popupValues.popupYes();
- }
-
- isDuplicateSkill(skillSet: string): boolean {
- if (!this.popupValues.skillsArray) return false;
- for (let i = 0; i < this.popupValues.skillsArray.length; i++) {
- if (this.popupValues.skillsArray[i].skillSet == skillSet && i != this.popupValues.currentIndex) {
- return true;
- }
- }
- return false;
- }
-
- popupNo() {
- this.skillsManagementService.changeSkillSet(null);
- this.popupValues.popupNo();
- }
-
- }
skills-management.component.html
- <div *ngIf="popupValues">
- <div class="modal" role="dialog">
- <div class="modal-dialog " role="document">
- <div class="modal-content">
- <div *ngIf="popupValues?.type == 'popup'" class="modal-body">
- <!-- header -->
- <div class="modal-header dis-blk ">
- <button type="button" class="close" data-dismiss="modal" (click)="popupNo()">
- <img src="assets/times.png" alt="">
- </button>
- </div>
- <div class="header-modal">
- <h1>{{modalHeaderLabel}}</h1>
- </div>
-
- <!-- body -->
- <div class="modal-body">
- <div class="dialogue_wrap">
- <form novalidate
- [formGroup]="skillSetForm">
- <div class="form-group row mb-3">
- <label class="col-md-5 col-form-label"
- for="skillId">Skillset</label>
- <div class="col-md-7">
- <select id="skillId" formControlName="skillSet" class="form-control">
- <option value="" disabled selected>Select a Skill</option>
- <option *ngFor="let skill of skillsArray" [value]="skill">{{skill}}</option>
- </select>
-
- </div>
- </div>
-
- <div class="form-group row mb-3">
- <label class="col-md-5 col-form-label"
- for="experienceId">Experience (in Months)</label>
- <div class="col-md-7">
- <input class="form-control" style="padding-left: 15px"
- id="experienceId"
- type="text"
- formControlName="experience" />
- </div>
- </div>
-
- <div class="form-group row mb3">
- <label class="col-md-5 col-form-label"
- for="proficiencyId">Proficiency</label>
- <div class="col-md-7">
- <select id="proficiencyId" formControlName="proficiency" class="form-control">
- <option value="" disabled selected>Select Proficiency</option>
- <option *ngFor="let proficiency of proficiencyArray" [value]="proficiency">{{proficiency}}</option>
- </select>
- </div>
- </div>
- </form>
- </div>
- </div>
- <!-- footer -->
- <div class="modal-footer">
- <div class="dialogue_btnwrap">
- <a (click)="popupNo()">
- <input type="button" class="btn select-button" value="Cancel" />
- </a>
- <a (click)="popupYes()">
- <input type="button" class="btn select-button" value="{{editMode}}" />
- </a>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
skills-management.component.css
We must add the selector of this component "app-skills-management" inside the “app.component.html” file. Otherwise the popup will not work.
- <body>
- <app-nav-menu></app-nav-menu>
- <app-skills-management></app-skills-management>
- <div class="container">
- <router-outlet></router-outlet>
- </div>
- </body>
Create an employee detail component inside “employee-detail” folder in the same way.
employee-detail.component.ts
- import { Component, OnInit } from '@angular/core';
- import { ActivatedRoute, Router } from '@angular/router';
- import { Employee } from '../data-models/employee';
- import { EmployeeService } from '../services/employee-service';
-
- @Component({
- selector: 'app-employee-detail',
- templateUrl: './employee-detail.component.html',
- styleUrls: ['./employee-detail.component.css']
- })
- export class EmployeeDetailComponent implements OnInit {
- pageTitle = 'Employee Detail';
- errorMessage = '';
- employee: Employee | undefined;
-
- constructor(private route: ActivatedRoute,
- private router: Router,
- private employeeService: EmployeeService) { }
-
- ngOnInit() {
- const id = this.route.snapshot.paramMap.get('id');
- const cityname = this.route.snapshot.paramMap.get('cityname');
- if (id && cityname) {
- this.getEmployee(id, cityname);
- }
- }
-
- getEmployee(id: string, cityName: string) {
- this.employeeService.getEmployee(id, cityName).subscribe(
- employee => this.employee = employee,
- error => this.errorMessage = <any>error);
- }
-
- onBack(): void {
- this.router.navigate(['/employees']);
- }
- }
employee-detail.component.html
- <div class="card">
- <div class="card-header"
- *ngIf="employee">
- {{pageTitle + ": " + employee.name}}
- </div>
- <div class="card-body"
- *ngIf="employee">
- <div class="row">
- <div class="col-md-6">
- <div class="row">
- <div class="col-md-3">Name:</div>
- <div class="col-md-6">{{employee.name}}</div>
- </div>
- <div class="row">
- <div class="col-md-3">City:</div>
- <div class="col-md-6">{{employee.cityname}}</div>
- </div>
- <div class="row">
- <div class="col-md-3">Address:</div>
- <div class="col-md-6">{{employee.address}}</div>
- </div>
- <div class="row">
- <div class="col-md-3">Gender:</div>
- <div class="col-md-6">{{employee.gender}}</div>
- </div>
- <div class="row">
- <div class="col-md-3">Company:</div>
- <div class="col-md-6">{{employee.company}}</div>
- </div>
- <div class="row">
- <div class="col-md-3">Designation:</div>
- <div class="col-md-6">{{employee.designation}}</div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="row employee-skillset-heading">
- Employee Skill Sets
- </div>
- <div class="row table-responsive" style="max-height:200px;">
- <table class="table mb-0">
- <thead>
- <tr>
- <th style="vertical-align: middle">Technology</th>
- <th style="vertical-align: middle">Experience (in Months)</th>
- <th style="vertical-align: middle">Proficiency</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let technologyStack of employee.technologyStacks">
- <td>{{ technologyStack.skillSet }}</td>
- <td>{{ technologyStack.experience }}</td>
- <td>{{ technologyStack.proficiency }}</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <div class="row mt-4">
- <div class="col-md-4">
- <button class="btn btn-outline-secondary mr-3"
- style="width:80px"
- (click)="onBack()">
- <i class="fa fa-chevron-left"></i> Back
- </button>
- <button class="btn btn-outline-primary"
- style="width:80px"
- [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">
- Edit
- </button>
- </div>
- </div>
- </div>
- <div class="alert alert-danger"
- *ngIf="errorMessage">
- {{errorMessage}}
- </div>
- </div>
employee-detail.component.css
- .employee-skillset-heading {
- margin-left: -5px;
- margin-bottom: 5px;
- font-weight: bold;
- color: cornflowerblue;
- }
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.
- import { BrowserModule } from '@angular/platform-browser';
- import { NgModule } from '@angular/core';
- import { FormsModule, ReactiveFormsModule } from '@angular/forms';
- import { HttpClientModule } from '@angular/common/http';
- import { RouterModule } from '@angular/router';
-
- import { AppComponent } from './app.component';
- import { NavMenuComponent } from './nav-menu/nav-menu.component';
- import { HomeComponent } from './home/home.component';
- import { EmployeeListComponent } from './employees/employee-list/employee-list.component';
- import { EmployeeDetailComponent } from './employees/employee-detail/employee-detail.component';
- import { EmployeeEditComponent } from './employees/employee-edit/employee-edit.component';
- import { EmployeeEditGuard } from './employees/employee-edit/employee-edit.guard';
- import { SkillsManagementComponent } from './employees/skills-management/skills-management.component';
-
- import { EmployeeService } from './employees/services/employee-service';
- import { SkillsManagementService } from './employees/skills-management/skills-management.service';
-
- @NgModule({
- declarations: [
- AppComponent,
- NavMenuComponent,
- HomeComponent,
- EmployeeListComponent,
- EmployeeDetailComponent,
- EmployeeEditComponent,
- SkillsManagementComponent
- ],
- imports: [
- BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
- HttpClientModule,
- FormsModule,
- ReactiveFormsModule,
- RouterModule.forRoot([
- { path: '', component: HomeComponent, pathMatch: 'full' },
- {
- path: 'employees',
- component: EmployeeListComponent
- },
- {
- path: 'employees/:id/:cityname',
- component: EmployeeDetailComponent
- },
- {
- path: 'employees/:id/:cityname/edit',
- canDeactivate: [EmployeeEditGuard],
- component: EmployeeEditComponent
- },
- ])
- ],
- providers: [
- EmployeeService,
- SkillsManagementService,
- ],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
We have completed the coding part of the application. We can execute the app now.
We can click the “Employees” menu and create a new employee record.
We can add new Technical skills by clicking “New Stack” button.
I have added three skills for the employee.
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.
Now we can check the local emulator and see the data stored in Cosmos DB as JSON format.
We can add one more employee record and see the data in the grid. You can even search the employee name in Filter textbox.
Click the employee name and see the employee detail in read-only mode.
You can also click the Delete button to remove the employee record. It will ask for a confirmation before deleting the data.
We have seen all the CRUD actions (except update; update is also implemented in the application) with this simple employee app.
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.