Categories
Angular

Add Keyboard Shortcut Feature to Your Angular App

Keyboard shortcuts are a very convenient feature for users. It allows them to do things without many clicks, increasing productivity. Keyboard shortcut handling can easily be added to Angular apps with the angular2-hotkeys library.

In this article, we will write a diet tracker app that lets users enter their calories eaten for a given day. They can use keyboard shortcuts to open the modal to add an entry and also to delete the latest entry. To start the project, we install the Angular CLI by running npm i -g @angular/cli. Next we run the Angular CLI to create the project by typing:

ng new diet-tracker

In the setup wizard, we choose to include routing and use SCSS as our CSS preprocessor.

Then we install some packages. We need the angular2-hotkeys package we mentioned above, Ngx-Bootstrap for styling, as well as MobX to store the data in a shared store. To install them, we run:

npm i ngx-bootatrap angular2-hotkeys mobx mobx-angular

Next we create our components and services. To do this, we run:

ng g component dietForm
ng g component homePage
ng g service diet
ng g class calorie
ng g class dietStore

Now we are ready to write some code. In diet-form.component.html, we replace the existing code with:

<form (ngSubmit)="save(dietForm)" #dietForm="ngForm">
  <div class="form-group">
    <label>Date</label>
    <input
      type="text"
      class="form-control"
      placeholder="Date"
      #date="ngModel"
      name="date"
      [(ngModel)]="form.date"
      required
      pattern="([12]d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01]))"
    />
    <div *ngIf="date?.invalid && (date.dirty || date.touched)">
      <div *ngIf="date.errors.required">
        Date is required.
      </div>
      <div *ngIf="date.invalid">
        Date is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Amount of Calories</label>
    <input
      type="text"
      class="form-control"
      placeholder="Amount"
      #amount="ngModel"
      name="amount"
      [(ngModel)]="form.amount"
      required
      pattern="^(d*.)?d+"
/>
    <div *ngIf="amount?.invalid && (amount.dirty || amount.touched)">
      <div *ngIf="amount.errors.required">
        Amount is required.
      </div>
      <div *ngIf="amount.invalid">
        Amount is invalid.
      </div>
    </div>
  </div>

  <button class="btn btn-primary">Save</button>
</form>

We add the form to let users enter the date they ate and the amount of calories eaten on the given day. We use Angular’s template driven form validation to check if everything is filled in, to verify that the date is in YYYY-MM-DD format, and to check if the calorie count is a non-negative number. We also have a Save button to save the data when it’s clicked. This form is used for both adding and editing entries.

Next in diet-form.component.ts, we replace the existing code with:

import { Component, OnInit, Output, EventEmitter, Input, SimpleChanges } from '@angular/core';
import { NgForm } from '@angular/forms';
import { DietService } from '../diet.service';
import { caloriesStore } from '../diet-store';
import { Calorie } from '../calorie';

@Component({
  selector: 'app-diet-form',
  templateUrl: './diet-form.component.html',
  styleUrls: ['./diet-form.component.scss']
})
export class DietFormComponent implements OnInit {
  form: any = <any>{};
  @Output('saved') saved = new EventEmitter();
  @Input() edit: boolean;
  @Input() selectedCalorie: any;
  store = caloriesStore;

constructor(private dietService: DietService) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    this.form = Object.assign({}, this.selectedCalorie);
  }

  save(dietForm: NgForm) {
    if (dietForm.invalid) {
      return;
    }
    if (this.edit) {
      this.dietService.editCaloriesEaten(this.form)
        .subscribe(res => {
          this.getCaloriesEaten()
          this.saved.emit();
        })
    }
    else {
      this.dietService.addCaloriesEaten(this.form)
        .subscribe(res => {
          this.getCaloriesEaten()
          this.saved.emit();
        })
    }
  }

  getCaloriesEaten() {
    this.dietService.getCaloriesEaten()
      .subscribe((res: Calorie[]) => {
        res = res.sort((a, b) => +new Date(a.date) - +new Date(b.date));
        this.store.setCalories(res);
      })
  }

}

This file has the functions that we called in the previous template like the save function. We also have the Inputs to get the data from the home page, as well as an Output to emit a saved event to the home page. Since we use the form for editing, we also need to pass in the selected entry with the selectedCalorie Input so that it can be edited. To update the form object with the selectedCalorie values, we copied the value whenever the selectedCalorie Input changes.

In the save function, we validate the form and call different functions for saving depending on whether the form is being used for adding or editing an entry. The latest entries will be populated in our MobX store by calling the getCaloriesEaten function and the saved event will be emitted once that’s done.

Next in home-page.component.html, we replace the code with:

<ng-template #addTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Add Calories Eaten</h2>
  </div>
  <div class="modal-body">
    <app-diet-form (saved)="closeModals()"></app-diet-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Edit Calories Eaten</h2>
  </div>
  <div class="modal-body">
    <app-diet-form
      [edit]="true"
      (saved)="closeModals()"
      [selectedCalorie]="selectedCalorie"
    ></app-diet-form>
  </div>
</ng-template>

<h1 class="text-center">Diet Tracker</h1>

<div class="btn-group" role="group">
  <button
    type="button"
    class="btn btn-secondary"
    (click)="openAddModal(addTemplate)"
  >
    Add Calories Eaten
  </button>
</div>

<div class="table-responsive">
  <br />
  <table class="table">
    <thead>
      <tr>
        <th scope="col">Date</th>
        <th scope="col">Calories Eaten</th>
        <th scope="col">Edit</th>
        <th scope="col">Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let c of store.calories">
        <th scope="row">{{ c.date | date }}</th>
        <td>{{ c.amount }}</td>
        <td>
          <button
            type="button"
            class="btn btn-secondary"
            (click)="openEditModal(editTemplate, c)"
          >
            Edit
          </button>
        </td>
        <td>
          <button
            type="button"
            class="btn btn-secondary"
            (click)="deleteCaloriesEaten(c.id)"
          >
            Delete
          </button>
        </td>
      </tr>
    </tbody>
  </table>
</div>

This creates buttons for adding, editing, and deleting entries, as well as a table for showing the entries. Also, we have the modals for adding and editing entries that we open with the Add and Edit buttons respectively.

Next in home-page.component.ts, we replace the existing code with:

import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { caloriesStore } from '../diet-store';
import { DietService } from '../diet.service';
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { Calorie } from '../calorie';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  addModalRef: BsModalRef;
  editModalRef: BsModalRef;
  selectedCalorie: any = <any>{};
  store = caloriesStore;
  @ViewChild('addTemplate', undefined) addTemplate: TemplateRef<any>;

  constructor(
    private modalService: BsModalService,
    private dietService: DietService,
    private _hotkeysService: HotkeysService
  ) { }

  ngOnInit() {
    this.getCaloriesEaten();
    this.addHotKeys();
  }

  addHotKeys() {
    this._hotkeysService.add(new Hotkey(['ctrl+shift+a', 'ctrl+shift+d'], (event: KeyboardEvent, combo: string): boolean => {
      if (combo === 'ctrl+shift+a') {
        this.openAddModal(this.addTemplate);
      }

      if (combo === 'ctrl+shift+d') {
        if (Array.isArray(this.store.calories) && this.store.calories[0]) {
          this.deleteCaloriesEaten(this.store.calories[0].id);
        }

}
      return false;
    }));
  }

  getCaloriesEaten() {
    this.dietService.getCaloriesEaten()
      .subscribe((res: any[]) => {
        res = res.sort((a, b) => +new Date(a.date) - +new Date(b.date));
        this.store.setCalories(res);
      })
  }

  openAddModal(template: TemplateRef<any>) {
    this.addModalRef = this.modalService.show(template);
  }

  openEditModal(template: TemplateRef<any>, calorie) {
    this.editModalRef = this.modalService.show(template);
    this.selectedCalorie = calorie;
  }

  closeModals() {
    this.addModalRef && this.addModalRef.hide();
    this.editModalRef && this.editModalRef.hide();
  }

  deleteCaloriesEaten(id) {
    this.dietService.deleteCaloriesEaten(id)
      .subscribe((res: Calorie[]) => {
        this.getCaloriesEaten();
      })
  }
}

We have the openAddModal and openEditModal functions to open the Add and Edit modals. The closeModals function is for closing the modals when things are saved in the app-diet-form component. The deleteCaloriesEaten function is for deleting the calories, and the getCaloriesEaten is used for getting the entries when the page loads and when items are deleted. It also puts the items in our store so every component can access it.

We also have the addHotKeys function to add the hotkeys to our app for the users convenience. The HotKeyService is from the angular2-hotkeys library. We inject it and then define the hotkeys. We defined Ctrl+Shift+A to open the add modal, and the Ctrl+Shift+D combo to delete the first entry on the list. The return false statement in the callback is to prevent the event from bubbling.

In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/route';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

This is so users can see the pages we just added when they click on the links or enter the URLs.

Next in app.component.html, we put:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" routerLink="/">Diet Tracker</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" routerLink="/">Home </a>
      </li>
    </ul>
  </div>
</nav>

<div class="page">
  <router-outlet></router-outlet>
</div>

This adds the links to our pages and exposes the router-outlet so users can see our pages.

Then in app.component.scss, we add:

.page {
  padding: 20px;
}

nav {
  background-color: lightsalmon !important;
}

This adds padding to our pages and changes the color of our Bootstrap navigation bar.

In app.module.ts, we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { DietFormComponent } from './diet-form/diet-form.component';
import { ModalModule } from 'ngx-bootstrap/modal';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { MobxAngularModule } from 'mobx-angular';
import { HotkeyModule } from 'angular2-hotkeys';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    DietFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ModalModule.forRoot(),
    MobxAngularModule,
    FormsModule,
    HttpClientModule,
    HotkeyModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This adds our components, services, and libraries that we use in our app.

In calorie.ts, we add:

export class Calorie {
    public date: string;
    public amount: string;
}

This adds types to our calorie form model.

Then in dietStore.ts, we add:

import { observable, action } from 'mobx-angular';

class CaloriesStore {
    @observable calories = [];
    @action setCalories(calories) {
        this.calories = calories;
    }
}

export const caloriesStore = new CaloriesStore();

This creates the MobX store to get our components and share the data. Whenever we call this.store.setCalories in our components ,we set the calories data in this store since we added the @action decorator before it. When we call this.store.calories in our component code, we are always getting the latest value from this store since it has the @observable decorator.

Then in diet.service.ts, we replace the existing code with:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';

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

  constructor(private http: HttpClient) { }

  getCaloriesEaten() {
    return this.http.get(`${environment.apiUrl}/calories`);
  }

  addCaloriesEaten(data) {
    return this.http.post(`${environment.apiUrl}/calories`, data);
  }

  editCaloriesEaten(data) {
    return this.http.put(`${environment.apiUrl}/calories/${data.id}`, data);
  }

  deleteCaloriesEaten(id) {
    return this.http.delete(`${environment.apiUrl}/calories/${id}`);
  }
}

This is so that we can make HTTP requests to our backend to get, save and delete the user’s entries.

Next in environment.prod.ts and environment.ts , we replace the existing code with:

export const environment = {
  production: true,
  apiUrl: 'http://localhost:3000'
};

This adds our API’s URL.

Finally, in index.html, we replace the code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Diet Tracker</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
      integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

to add the Bootstrap CSS and JavaScript dependencies into our app, as well as changing the title.

After all the work, we can run ng serve to run the app.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "calories": [
  ]
}

So we have the calories endpoints defined in the requests.js available.

Categories
Angular

How to Use MobX for Easy State Management in Your Angular App

Angular is a comprehensive frontend framework for building web applications. It comes with a lot of great features like component-based architectures, communication between parent and child components, routing, guards for controlling access to routes, services for shared code, and TypeScript for easy development. However, one big feature that it does not come with it is global state management for the app.

MobX is a simple state management library. It works by allowing any code that references the MobX store to observe values and trigger a change when that value changes. The store also exposes functions that you can use to set the values.

To use MobX, you just import the store into the component where you want to use it, so there’s no boilerplate to add until NgRX/store. This means that the state management is much simpler than with NgRX/store. There is no need for dispatch functions and reducers with MobX.

Angular and MobX work great together because of the MobX-Angular library.

In this article, we will build an address book app that stores contacts in a backend. We use MobX-Angular to share data between components. For styling, we use Ngx-Boostrap library to style the app.

We start by installing the Angular CLI by running npm i -g @angular/cli. Next we run ng new address-book-app to create the files for our address book app. Be sure to choose to include the routing and SCSS for styling.

Next we install a few libraries that we need to use. We MobX and MobX-Angular for state management, Ng2-Validation for form validation, and Ngx-Bootstrap for styling.

After all the libraries are installed, we run a few commands to create more code files.

ng g component contactForm
ng g component homePage
ng g class contactStore
ng g service contacts
ng g class exports

This will create files for our home page, contact form, and a service for sending HTTP requests to our server,

Now in contact-form.component.ts, we put:

import { Component, OnInit, Input, Output, SimpleChanges } from '@angular/core';
import { COUNTRIES } from '../exports';
import { contactStore } from '../contact-store';
import { EventEmitter } from '@angular/core';
import { ContactsService } from '../contacts.service';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.scss']
})
export class ContactFormComponent implements OnInit {
  contactData: any = <any>{};
  countries = COUNTRIES;
  store = contactStore;
  @Input('edit') edit: boolean;
  @Input('contact') contact: any = <any>{};
  @Output('contactEdited') contactEdited = new EventEmitter();

  constructor(
    private contactsService: ContactsService
  ) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    if (this.contact) {
      this.contactData = Object.assign({}, this.contact);
    }
  }

  getPostalCodeRegex() {
    if (this.contactData.country == "United States") {
      return /^[0-9]{5}(?:-[0-9]{4})?$/;
    } else if (this.contactData.country == "Canada") {
      return /^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/;
    }
    return /./;
  }

  getPhoneRegex() {
    if (["United States", "Canada"].includes(this.contactData.country)) {
      return /^[2-9]d{2}[2-9]d{2}d{4}$/;
    }
    return /./;
  }

  saveContact(contactForm: NgForm) {
    if (contactForm.invalid) {
      return;
    }

    if (this.edit) {
      this.contactsService.editContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
    else {
      this.contactsService.addContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
        this.contactEdited.emit();
      })
  }

}

This is the logic for the contact form. We created functions for getting the regular expression for the phone and postal code depending on the country, getPostalCodeRegex and getPhoneRegex respectively. Also we have a function for saving our contact and getting it after it’s saved, the saveContact and getContacts functions respectively. We need the ngOnChanges life cycle handler to get an existing contact if it’s passed in.

Whenever we get contacts, we call this.store.setContacts where this.store is the MobX store that we will create, and it sets the latest contacts array to the store.

In contact-form.component.html, we add:

<form #contactForm="ngForm" (ngSubmit)="saveContact(contactForm)">
  <div class="form-group">
    <label>First Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="First Name"
      #firstName="ngModel"
      name="firstName"
      [(ngModel)]="contactData.firstName"
      required
    />
    <div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)">
      <div *ngIf="firstName.errors.required">
        First Name is required.
      </div>
    </div>
  </div>
  <div class="form-group">
    <label>Last Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="Last Name"
      #lastName="ngModel"
      name="lastName"
      [(ngModel)]="contactData.lastName"
      required
    />
    <div *ngIf="lastName?.invalid && (lastName.dirty || lastName.touched)">
      <div *ngIf="lastName.errors.required">
        Last Name is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 1</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 1"
      #addressLineOne="ngModel"
      name="addressLineOne"
      [(ngModel)]="contactData.addressLineOne"
      required
    />
    <div
      *ngIf="
        addressLineOne?.invalid &&
        (addressLineOne.dirty || addressLineOne.touched)
      "
    >
      <div *ngIf="addressLineOne.errors.required">
        Address line 1 is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 2</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 2"
      #addressLineTwo="ngModel"
      name="addressLineTwo"
      [(ngModel)]="contactData.addressLineTwo"
    />
  </div>

  <div class="form-group">
    <label>City</label>
    <input
      type="text"
      class="form-control"
      placeholder="City"
      #city="ngModel"
      name="city"
      [(ngModel)]="contactData.city"
      required
    />
    <div *ngIf="city?.invalid && (city.dirty || city.touched)">
      <div *ngIf="city.errors.required">
        City is required.
      </div>
      <div *ngIf="city.invalid">
        City is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Country</label>
    <select
      class="form-control"
      #country="ngModel"
      name="country"
      [(ngModel)]="contactData.country"
      required
    >
      <option *ngFor="let c of countries" [value]="c.name">
        {{ c.name }}
      </option>
    </select>
    <div *ngIf="country?.invalid && (country.dirty || country.touched)">
      <div *ngIf="country.errors.required">
        Country is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Postal Code</label>
    <input
      type="text"
      class="form-control"
      placeholder="Postal Code"
      #postalCode="ngModel"
      name="postalCode"
      [(ngModel)]="contactData.postalCode"
      required
      [pattern]="getPostalCodeRegex()"
    />
    <div
      *ngIf="postalCode?.invalid && (postalCode.dirty || postalCode.touched)"
    >
      <div *ngIf="postalCode.errors.required">
        Postal code is required.
      </div>
      <div *ngIf="postalCode.invalid">
        Postal code is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Phone</label>
    <input
      type="text"
      class="form-control"
      placeholder="Phone"
      #phone="ngModel"
      name="phone"
      [(ngModel)]="contactData.phone"
      required
      [pattern]="getPhoneRegex()"
    />
    <div *ngIf="phone?.invalid && (phone.dirty || phone.touched)">
      <div *ngIf="phone.errors.required">
        Phone is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Age</label>
    <input
      type="text"
      class="form-control"
      placeholder="Age"
      #age="ngModel"
      name="age"
      [(ngModel)]="contactData.age"
      required
      [range]="[0, 200]"
    />
    <div *ngIf="age?.invalid && (age.dirty || age.touched)">
      <div *ngIf="age.errors.required">
        Age is required.
      </div>
      <div *ngIf="age.invalid">
        Age must be between 0 and 200.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Email</label>
    <input
      type="text"
      class="form-control"
      placeholder="Email"
      #email="ngModel"
      name="email"
      [(ngModel)]="contactData.email"
      required
      email
    />
    <div *ngIf="email?.invalid && (email.dirty || email.touched)">
      <div *ngIf="email.errors.required">
        Email is required.
      </div>
      <div *ngIf="email.invalid">
        Email is invalid.
      </div>
    </div>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

This is the contact form itself. The email directive is provided by ng2-validation for validating email. Also the [range]=”[0, 200]” input is also provided by the same library for validation number range.

Next we work on the home page. In home-page.component.ts, we add:

import { Component, OnInit, TemplateRef } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { ContactsService } from '../contacts.service';
import { contactStore } from '../contact-store';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {

  modalRef: BsModalRef;
  store = contactStore;
  edit: boolean;
  contacts: any[] = [];
  selectedContact: any = <any>{};
  constructor(
    private modalService: BsModalService,
    private contactsService: ContactsService
  ) { }

  openModal(addTemplate: TemplateRef<any>) {
    this.modalRef = this.modalService.show(addTemplate);
  }

  openEditModal(editTemplate: TemplateRef<any>, contact) {
    this.selectedContact = contact;
    this.modalRef = this.modalService.show(editTemplate);
  }

  closeModal() {
    this.modalRef.hide();
    this.selectedContact = {};
  }

  ngOnInit() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }

  deleteContact(id) {
    this.contactsService.deleteContact(id)
      .subscribe(res => {
        this.getContacts();
      })
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }
}

We have the getContacts function to get contacts, deleteContact function to delete a contact, and openModal / openEditModal functions to open the add contact and edit contact modals respectively. The closeModal function is used to close the modal with the contactEdited event emitted from ContactFormComponent .

Whenever we get contacts, we call this.store.setContacts to set the latest contacts array to the store.

Next, in home-page.component.html we add:

<div class="home-page" *mobxAutorun>
  <h1 class="text-center">Address Book</h1>
  <button
    type="button"
    class="btn btn-primary"
    (click)="openModal(addTemplate)"
  >
    Add Contact
  </button>

  <div class="table-container">
    <table class="table">
      <thead>
        <tr>
          <th>First Name</th>
          <th>Last Name</th>
          <th>Address</th>
          <th>Phone</th>
          <th>Age</th>
          <th>Email</th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let c of store.contacts">
          <td>{{ c.firstName }}</td>
          <td>{{ c.lastName }}</td>
          <td>
            {{ c.addressLineOne }}, {{ c.city }},
            {{ c.country }}
          </td>
          <td>{{ c.phone }}</td>
          <td>{{ c.age }}</td>
          <td>{{ c.email }}</td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="openEditModal(editTemplate, c)"
            >
              Edit
            </button>
          </td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="deleteContact(c.id)"
            >
              Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

<ng-template #addTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Add Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="false"
    ></app-contact-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Edit Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="true"
      [contact]='selectedContact'
    ></app-contact-form>
  </div>
</ng-template>

We have a table to display the list of contacts. The *ngFor=”let c of store.contacts” directive is in the tr tag to loop through the contacts store them in our MobX store.

The ng-template components are modals provided by ngx-bootstrap. We have addTemplate for using the contact form to add a contact, and editTemplate to display the same form for editing.

In home-page.component.scss, we add:

.home-page {
    padding: 20px;
}

.table-container {
    margin-top: 20px;
}

In app-routing.module.ts, which we get by including the routing when running the Angular CLI wizard, we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

This allows users to navigate to the home page.

In app.component.html, we replace the existing code with:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Address Book</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home </a>
      </li>
    </ul>
  </div>
</nav>
<router-outlet></router-outlet>

Here we add the Bootstrap navigation bar for showing the app title and navigation links.

Next in app.module.ts, we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ContactFormComponent } from './contact-form/contact-form.component';
import { ModalModule } from 'ngx-bootstrap/modal';
import { CustomFormsModule } from 'ng2-validation'
import { MobxAngularModule } from 'mobx-angular';
import { ContactsService } from './contacts.service';

@NgModule)({
  declarations: [
    AppComponent,
    HomePageComponent,
    ContactFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    CustomFormsModule,
    ModalModule.forRoot(),
    MobxAngularModule
  ],
  providers: [
    ContactsService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

This includes all the libraries we use for building the app along with the code we generated.

Then we create the MobX store. In contact-store.ts, we add:

import { observable, action } from 'mobx-angular';

class ContactStore {
    @observable contacts = [];
    @action setContacts(contacts) {
        this.contacts = contacts;
    }
}

export const contactStore = new ContactStore();

The store is very simple. It has the contacts array which we decorate with the observable decorator so that it will be always updated everywhere when we reference it in our code. The setContacts function lets us set the value of the contacts array which will be propagated to any code that references it since we have the observable decorator in front of it. The action decorator means that we designate the function to be able to update observable values in the store.

We export the instance of the store, which we use in our components wherever we reference contactStore .

Next in contact.service.ts, we add:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';

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

constructor(
    private http: HttpClient
  ) { }

  getContacts() {
    return this.http.get(`${environment.apiUrl}/contacts`);
  }

  addContact(data) {
    return this.http.post(`${environment.apiUrl}/contacts`, data);
  }

  editContact(data) {
    return this.http.put(`${environment.apiUrl}/contacts/${data.id}`, data);
  }

  deleteContact(id) {
    return this.http.delete(`${environment.apiUrl}/contacts/${id}`);
  }
}

This lets make HTTP requests to our backend for manipulating contacts.

In exports.ts, we add:

export const COUNTRIES = [
    { "name": "Afghanistan", "code": "AF" },
    { "name": "Aland Islands", "code": "AX" },
    { "name": "Albania", "code": "AL" },
    { "name": "Algeria", "code": "DZ" },
    { "name": "American Samoa", "code": "AS" },
    { "name": "AndorrA", "code": "AD" },
    { "name": "Angola", "code": "AO" },
    { "name": "Anguilla", "code": "AI" },
    { "name": "Antarctica", "code": "AQ" },
    { "name": "Antigua and Barbuda", "code": "AG" },
    { "name": "Argentina", "code": "AR" },
    { "name": "Armenia", "code": "AM" },
    { "name": "Aruba", "code": "AW" },
    { "name": "Australia", "code": "AU" },
    { "name": "Austria", "code": "AT" },
    { "name": "Azerbaijan", "code": "AZ" },
    { "name": "Bahamas", "code": "BS" },
    { "name": "Bahrain", "code": "BH" },
    { "name": "Bangladesh", "code": "BD" },
    { "name": "Barbados", "code": "BB" },
    { "name": "Belarus", "code": "BY" },
    { "name": "Belgium", "code": "BE" },
    { "name": "Belize", "code": "BZ" },
    { "name": "Benin", "code": "BJ" },
    { "name": "Bermuda", "code": "BM" },
    { "name": "Bhutan", "code": "BT" },
    { "name": "Bolivia", "code": "BO" },
    { "name": "Bosnia and Herzegovina", "code": "BA" },
    { "name": "Botswana", "code": "BW" },
    { "name": "Bouvet Island", "code": "BV" },
    { "name": "Brazil", "code": "BR" },
    { "name": "British Indian Ocean Territory", "code": "IO" },
    { "name": "Brunei Darussalam", "code": "BN" },
    { "name": "Bulgaria", "code": "BG" },
    { "name": "Burkina Faso", "code": "BF" },
    { "name": "Burundi", "code": "BI" },
    { "name": "Cambodia", "code": "KH" },
    { "name": "Cameroon", "code": "CM" },
    { "name": "Canada", "code": "CA" },
    { "name": "Cape Verde", "code": "CV" },
    { "name": "Cayman Islands", "code": "KY" },
    { "name": "Central African Republic", "code": "CF" },
    { "name": "Chad", "code": "TD" },
    { "name": "Chile", "code": "CL" },
    { "name": "China", "code": "CN" },
    { "name": "Christmas Island", "code": "CX" },
    { "name": "Cocos (Keeling) Islands", "code": "CC" },
    { "name": "Colombia", "code": "CO" },
    { "name": "Comoros", "code": "KM" },
    { "name": "Congo", "code": "CG" },
    { "name": "Congo, The Democratic Republic of the", "code": "CD" },
    { "name": "Cook Islands", "code": "CK" },
    { "name": "Costa Rica", "code": "CR" },
    {
        "name": "Cote D"Ivoire", "code": "CI"
    },
    { "name": "Croatia", "code": "HR" },
    { "name": "Cuba", "code": "CU" },
    { "name": "Cyprus", "code": "CY" },
    { "name": "Czech Republic", "code": "CZ" },
    { "name": "Denmark", "code": "DK" },
    { "name": "Djibouti", "code": "DJ" },
    { "name": "Dominica", "code": "DM" },
    { "name": "Dominican Republic", "code": "DO" },
    { "name": "Ecuador", "code": "EC" },
    { "name": "Egypt", "code": "EG" },
    { "name": "El Salvador", "code": "SV" },
    { "name": "Equatorial Guinea", "code": "GQ" },
    { "name": "Eritrea", "code": "ER" },
    { "name": "Estonia", "code": "EE" },
    { "name": "Ethiopia", "code": "ET" },
    { "name": "Falkland Islands (Malvinas)", "code": "FK" },
    { "name": "Faroe Islands", "code": "FO" },
    { "name": "Fiji", "code": "FJ" },
    { "name": "Finland", "code": "FI" },
    { "name": "France", "code": "FR" },
    { "name": "French Guiana", "code": "GF" },
    { "name": "French Polynesia", "code": "PF" },
    { "name": "French Southern Territories", "code": "TF" },
    { "name": "Gabon", "code": "GA" },
    { "name": "Gambia", "code": "GM" },
    { "name": "Georgia", "code": "GE" },
    { "name": "Germany", "code": "DE" },
    { "name": "Ghana", "code": "GH" },
    { "name": "Gibraltar", "code": "GI" },
    { "name": "Greece", "code": "GR" },
    { "name": "Greenland", "code": "GL" },
    { "name": "Grenada", "code": "GD" },
    { "name": "Guadeloupe", "code": "GP" },
    { "name": "Guam", "code": "GU" },
    { "name": "Guatemala", "code": "GT" },
    { "name": "Guernsey", "code": "GG" },
    { "name": "Guinea", "code": "GN" },
    { "name": "Guinea-Bissau", "code": "GW" },
    { "name": "Guyana", "code": "GY" },
    { "name": "Haiti", "code": "HT" },
    { "name": "Heard Island and Mcdonald Islands", "code": "HM" },
    { "name": "Holy See (Vatican City State)", "code": "VA" },
    { "name": "Honduras", "code": "HN" },
    { "name": "Hong Kong", "code": "HK" },
    { "name": "Hungary", "code": "HU" },
    { "name": "Iceland", "code": "IS" },
    { "name": "India", "code": "IN" },
    { "name": "Indonesia", "code": "ID" },
    { "name": "Iran, Islamic Republic Of", "code": "IR" },
    { "name": "Iraq", "code": "IQ" },
    { "name": "Ireland", "code": "IE" },
    { "name": "Isle of Man", "code": "IM" },
    { "name": "Israel", "code": "IL" },
    { "name": "Italy", "code": "IT" },
    { "name": "Jamaica", "code": "JM" },
    { "name": "Japan", "code": "JP" },
    { "name": "Jersey", "code": "JE" },
    { "name": "Jordan", "code": "JO" },
    { "name": "Kazakhstan", "code": "KZ" },
    { "name": "Kenya", "code": "KE" },
    { "name": "Kiribati", "code": "KI" },
    {
        "name": "Korea, Democratic People"S Republic of", "code": "KP"
    },
    { "name": "Korea, Republic of", "code": "KR" },
    { "name": "Kuwait", "code": "KW" },
    { "name": "Kyrgyzstan", "code": "KG" },
    {
        "name": "Lao People"S Democratic Republic", "code": "LA"
    },
    { "name": "Latvia", "code": "LV" },
    { "name": "Lebanon", "code": "LB" },
    { "name": "Lesotho", "code": "LS" },
    { "name": "Liberia", "code": "LR" },
    { "name": "Libyan Arab Jamahiriya", "code": "LY" },
    { "name": "Liechtenstein", "code": "LI" },
    { "name": "Lithuania", "code": "LT" },
    { "name": "Luxembourg", "code": "LU" },
    { "name": "Macao", "code": "MO" },
    { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },
    { "name": "Madagascar", "code": "MG" },
    { "name": "Malawi", "code": "MW" },
    { "name": "Malaysia", "code": "MY" },
    { "name": "Maldives", "code": "MV" },
    { "name": "Mali", "code": "ML" },
    { "name": "Malta", "code": "MT" },
    { "name": "Marshall Islands", "code": "MH" },
    { "name": "Martinique", "code": "MQ" },
    { "name": "Mauritania", "code": "MR" },
    { "name": "Mauritius", "code": "MU" },
    { "name": "Mayotte", "code": "YT" },
    { "name": "Mexico", "code": "MX" },
    { "name": "Micronesia, Federated States of", "code": "FM" },
    { "name": "Moldova, Republic of", "code": "MD" },
    { "name": "Monaco", "code": "MC" },
    { "name": "Mongolia", "code": "MN" },
    { "name": "Montenegro", "code": "ME" },
    { "name": "Montserrat", "code": "MS" },
    { "name": "Morocco", "code": "MA" },
    { "name": "Mozambique", "code": "MZ" },
    { "name": "Myanmar", "code": "MM" },
    { "name": "Namibia", "code": "NA" },
    { "name": "Nauru", "code": "NR" },
    { "name": "Nepal", "code": "NP" },
    { "name": "Netherlands", "code": "NL" },
    { "name": "Netherlands Antilles", "code": "AN" },
    { "name": "New Caledonia", "code": "NC" },
    { "name": "New Zealand", "code": "NZ" },
    { "name": "Nicaragua", "code": "NI" },
    { "name": "Niger", "code": "NE" },
    { "name": "Nigeria", "code": "NG" },
    { "name": "Niue", "code": "NU" },
    { "name": "Norfolk Island", "code": "NF" },
    { "name": "Northern Mariana Islands", "code": "MP" },
    { "name": "Norway", "code": "NO" },
    { "name": "Oman", "code": "OM" },
    { "name": "Pakistan", "code": "PK" },
    { "name": "Palau", "code": "PW" },
    { "name": "Palestinian Territory, Occupied", "code": "PS" },
    { "name": "Panama", "code": "PA" },
    { "name": "Papua New Guinea", "code": "PG" },
    { "name": "Paraguay", "code": "PY" },
    { "name": "Peru", "code": "PE" },
    { "name": "Philippines", "code": "PH" },
    { "name": "Pitcairn", "code": "PN" },
    { "name": "Poland", "code": "PL" },
    { "name": "Portugal", "code": "PT" },
    { "name": "Puerto Rico", "code": "PR" },
    { "name": "Qatar", "code": "QA" },
    { "name": "Reunion", "code": "RE" },
    { "name": "Romania", "code": "RO" },
    { "name": "Russian Federation", "code": "RU" },
    { "name": "RWANDA", "code": "RW" },
    { "name": "Saint Helena", "code": "SH" },
    { "name": "Saint Kitts and Nevis", "code": "KN" },
    { "name": "Saint Lucia", "code": "LC" },
    { "name": "Saint Pierre and Miquelon", "code": "PM" },
    { "name": "Saint Vincent and the Grenadines", "code": "VC" },
    { "name": "Samoa", "code": "WS" },
    { "name": "San Marino", "code": "SM" },
    { "name": "Sao Tome and Principe", "code": "ST" },
    { "name": "Saudi Arabia", "code": "SA" },
    { "name": "Senegal", "code": "SN" },
    { "name": "Serbia", "code": "RS" },
    { "name": "Seychelles", "code": "SC" },
    { "name": "Sierra Leone", "code": "SL" },
    { "name": "Singapore", "code": "SG" },
    { "name": "Slovakia", "code": "SK" },
    { "name": "Slovenia", "code": "SI" },
    { "name": "Solomon Islands", "code": "SB" },
    { "name": "Somalia", "code": "SO" },
    { "name": "South Africa", "code": "ZA" },
    { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },
    { "name": "Spain", "code": "ES" },
    { "name": "Sri Lanka", "code": "LK" },
    { "name": "Sudan", "code": "SD" },
    { "name": "Suriname", "code": "SR" },
    { "name": "Svalbard and Jan Mayen", "code": "SJ" },
    { "name": "Swaziland", "code": "SZ" },
    { "name": "Sweden", "code": "SE" },
    { "name": "Switzerland", "code": "CH" },
    { "name": "Syrian Arab Republic", "code": "SY" },
    { "name": "Taiwan, Province of China", "code": "TW" },
    { "name": "Tajikistan", "code": "TJ" },
    { "name": "Tanzania, United Republic of", "code": "TZ" },
    { "name": "Thailand", "code": "TH" },
    { "name": "Timor-Leste", "code": "TL" },
    { "name": "Togo", "code": "TG" },
    { "name": "Tokelau", "code": "TK" },
    { "name": "Tonga", "code": "TO" },
    { "name": "Trinidad and Tobago", "code": "TT" },
    { "name": "Tunisia", "code": "TN" },
    { "name": "Turkey", "code": "TR" },
    { "name": "Turkmenistan", "code": "TM" },
    { "name": "Turks and Caicos Islands", "code": "TC" },
    { "name": "Tuvalu", "code": "TV" },
    { "name": "Uganda", "code": "UG" },
    { "name": "Ukraine", "code": "UA" },
    { "name": "United Arab Emirates", "code": "AE" },
    { "name": "United Kingdom", "code": "GB" },
    { "name": "United States", "code": "US" },
    { "name": "United States Minor Outlying Islands", "code": "UM" },
    { "name": "Uruguay", "code": "UY" },
    { "name": "Uzbekistan", "code": "UZ" },
    { "name": "Vanuatu", "code": "VU" },
    { "name": "Venezuela", "code": "VE" },
    { "name": "Viet Nam", "code": "VN" },
    { "name": "Virgin Islands, British", "code": "VG" },
    { "name": "Virgin Islands, U.S.", "code": "VI" },
    { "name": "Wallis and Futuna", "code": "WF" },
    { "name": "Western Sahara", "code": "EH" },
    { "name": "Yemen", "code": "YE" },
    { "name": "Zambia", "code": "ZM" },
    { "name": "Zimbabwe", "code": "ZW" }
]

So that we have a list of countries for the Countries drop down in our contact form.

In environment.ts, we add:

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'
};

This allows us to talk to our back end.

Then in index.html, we have:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Angular Address Book App</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="[https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css](https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css)"
      rel="stylesheet"
    />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

This changes the default title to our own and added Bootstrap CSS.

To run our app, we use the JSON server package located at https://github.com/typicode/json-server to create a simple backend without writing any code. Run npm i -g json-server to install it.

We create a file called db.json in the root folder and add:

{
  "contacts": []
}

This automatically generates the routes that we previously specified in contacts.service.ts.

Then we run json-server --watch db.json to start our back end, and run ng serve on to start our app.

Categories
Angular Bootstrap

How to Use Bootstrap in Your Angular App

Bootstrap is a popular library for styling web pages. We can use it to style Angular apps easily with ready made Bootstrap components for Angular. ngx-bootstrap is one of the most popular Bootstrap library for Angular. We can use it to style our apps easily.

ngx-bootstrap provides things that are harder to add with plain Bootstrap to an Angular app like modals. Common things like navigation and forms are not included, and so we use regular Bootstrap classes to style those.

In this article, we will build an address book app that stores contacts in a back end. We use MobX-Angular to share data between components. For styling, we use ngx-bootstrap library to style the app.

MobX is a simple state management library for state management. It works by allowing any code that references the MobX store to observe values that the store tracks. The store also has functions that you can use to set the values you want in the store.

You just import the store into the place where you want to use it, so there’s no boilerplate to add until NgRX/store. This means that the state management is much simpler than with NgRX/store. There is no need for dispatch functions and reducers with MobX.

Angular and MobX works great together because of the MobX-Angular library.

We start by installing the Angular CLI by running npm i -g @angular/cli . Next we run ng new address-book-app to create the files for our address book app. Be sure to choose to including routing and use SCSS for styling.

Next we install a few libraries that we need to use. We MobX and MobX-Angular for state management, Ng2-Validation for form validation of number and email fields, and Ngx-Bootstrap for styling.

After all the libraries are installed, we run a few commands to create more code files.

ng g component contactForm
ng g component homePage
ng g class contactStore
ng g service contacts
ng g class exports

This will create files for our home page, contact form, and a service for sending HTTP requests to our server,

Now in contact-form.component.ts , we put:

import { Component, OnInit, Input, Output, SimpleChanges } from '@angular/core';
import { COUNTRIES } from '../exports';
import { contactStore } from '../contact-store';
import { EventEmitter } from '@angular/core';
import { ContactsService } from '../contacts.service';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.scss']
})
export class ContactFormComponent implements OnInit {
  contactData: any = <any>{};
  countries = COUNTRIES;
  store = contactStore;
  @Input('edit') edit: boolean;
  @Input('contact') contact: any = <any>{};
  @Output('contactEdited') contactEdited = new EventEmitter();

  constructor(
    private contactsService: ContactsService
  ) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    if (this.contact) {
      this.contactData = Object.assign({}, this.contact);
    }
  }

  getPostalCodeRegex() {
    if (this.contactData.country == "United States") {
      return /^[0-9]{5}(?:-[0-9]{4})?$/;
    } else if (this.contactData.country == "Canada") {
      return /^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/;
    }
    return /./;
  }

  getPhoneRegex() {
    if (["United States", "Canada"].includes(this.contactData.country)) {
      return /^[2-9]d{2}[2-9]d{2}d{4}$/;
    }
    return /./;
  }

  saveContact(contactForm: NgForm) {
    if (contactForm.invalid) {
      return;
    }

    if (this.edit) {
      this.contactsService.editContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
    else {
      this.contactsService.addContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
        this.contactEdited.emit();
      })
  }

}

This is the logic for the contact form for our contact form. We functions for getting the regular expression for phone and postal code depending on country, getPostalCodeRegex and getPhoneRegex respectively. Also we have a function for saving our contact and getting it after it’s saved, the saveContact and getContacts functions respectively. We need the ngOnChanges life cycle handler to get existing contact if it’s passed in.

Whenever we get contacts, we call this.store.setContacts , where this.store is the MobX store that we will create, to set the latest contacts array to the store.

In contact-form.component.html , we add:

<form #contactForm="ngForm" (ngSubmit)="saveContact(contactForm)">
  <div class="form-group">
    <label>First Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="First Name"
      #firstName="ngModel"
      name="firstName"
      [(ngModel)]="contactData.firstName"
      required
    />
    <div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)">
      <div *ngIf="firstName.errors.required">
        First Name is required.
      </div>
    </div>
  </div>
  <div class="form-group">
    <label>Last Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="Last Name"
      #lastName="ngModel"
      name="lastName"
      [(ngModel)]="contactData.lastName"
      required
    />
    <div *ngIf="lastName?.invalid && (lastName.dirty || lastName.touched)">
      <div *ngIf="lastName.errors.required">
        Last Name is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 1</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 1"
      #addressLineOne="ngModel"
      name="addressLineOne"
      [(ngModel)]="contactData.addressLineOne"
      required
    />
    <div
      *ngIf="
        addressLineOne?.invalid &&
        (addressLineOne.dirty || addressLineOne.touched)
      "
    >
      <div *ngIf="addressLineOne.errors.required">
        Address line 1 is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 2</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 2"
      #addressLineTwo="ngModel"
      name="addressLineTwo"
      [(ngModel)]="contactData.addressLineTwo"
    />
  </div>

  <div class="form-group">
    <label>City</label>
    <input
      type="text"
      class="form-control"
      placeholder="City"
      #city="ngModel"
      name="city"
      [(ngModel)]="contactData.city"
      required
    />
    <div *ngIf="city?.invalid && (city.dirty || city.touched)">
      <div *ngIf="city.errors.required">
        City is required.
      </div>
      <div *ngIf="city.invalid">
        City is invalid.
      </div>
    </div>
  </div>

<div class="form-group">
    <label>Country</label>
    <select
      class="form-control"
      #country="ngModel"
      name="country"
      [(ngModel)]="contactData.country"
      required
    >
      <option *ngFor="let c of countries" [value]="c.name">
        {{ c.name }}
      </option>
    </select>
    <div *ngIf="country?.invalid && (country.dirty || country.touched)">
      <div *ngIf="country.errors.required">
        Country is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Postal Code</label>
    <input
      type="text"
      class="form-control"
      placeholder="Postal Code"
      #postalCode="ngModel"
      name="postalCode"
      [(ngModel)]="contactData.postalCode"
      required
      [pattern]="getPostalCodeRegex()"
    />
    <div
      *ngIf="postalCode?.invalid && (postalCode.dirty || postalCode.touched)"
    >
      <div *ngIf="postalCode.errors.required">
        Postal code is required.
      </div>
      <div *ngIf="postalCode.invalid">
        Postal code is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Phone</label>
    <input
      type="text"
      class="form-control"
      placeholder="Phone"
      #phone="ngModel"
      name="phone"
      [(ngModel)]="contactData.phone"
      required
      [pattern]="getPhoneRegex()"
    />
    <div *ngIf="phone?.invalid && (phone.dirty || phone.touched)">
      <div *ngIf="phone.errors.required">
        Phone is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Age</label>
    <input
      type="text"
      class="form-control"
      placeholder="Age"
      #age="ngModel"
      name="age"
      [(ngModel)]="contactData.age"
      required
      [range]="[0, 200]"
    />
    <div *ngIf="age?.invalid && (age.dirty || age.touched)">
      <div *ngIf="age.errors.required">
        Age is required.
      </div>
      <div *ngIf="age.invalid">
        Age must be between 0 and 200.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Email</label>
    <input
      type="text"
      class="form-control"
      placeholder="Email"
      #email="ngModel"
      name="email"
      [(ngModel)]="contactData.email"
      required
      email
    />
    <div *ngIf="email?.invalid && (email.dirty || email.touched)">
      <div *ngIf="email.errors.required">
        Email is required.
      </div>
      <div *ngIf="email.invalid">
        Email is invalid.
      </div>
    </div>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

This is the contact form itself. The email directive is provided by ng2-validation for validating email. Also the [range]=”[0, 200]” input is also provided by the same library for validation number range.

Next we work on the home page. In home-page.component.ts , we add:

import { Component, OnInit, TemplateRef } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { ContactsService } from '../contacts.service';
import { contactStore } from '../contact-store';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {

modalRef: BsModalRef;
  store = contactStore;
  edit: boolean;
  contacts: any[] = [];
  selectedContact: any = <any>{};
  constructor(
    private modalService: BsModalService,
    private contactsService: ContactsService
  ) { }

  openModal(addTemplate: TemplateRef<any>) {
    this.modalRef = this.modalService.show(addTemplate);
  }

  openEditModal(editTemplate: TemplateRef<any>, contact) {
    this.selectedContact = contact;
    this.modalRef = this.modalService.show(editTemplate);
  }

  closeModal() {
    this.modalRef.hide();
    this.selectedContact = {};
  }

  ngOnInit() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }

  deleteContact(id) {
    this.contactsService.deleteContact(id)
      .subscribe(res => {
        this.getContacts();
      })
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }
}

We have the getContacts function to get contacts, deleteContact function to delete a contact, and openModal , openEditModal functions to open the add contact and edit contact modals respectively. The closeModal function is used to close the modal with the contactEdited event is emitted from ContactFormComponent .

Whenever we get contacts, we call this.store.setContacts , where this.store is the MobX store that we will create, to set the latest contacts array to the store.

Next in home-page.component.html , we put:

<div class="home-page" *mobxAutorun>
  <h1 class="text-center">Address Book</h1>
  <button
    type="button"
    class="btn btn-primary"
    (click)="openModal(addTemplate)"
  >
    Add Contact
  </button>

  <div class="table-container">
    <table class="table">
      <thead>
        <tr>
          <th>First Name</th>
          <th>Last Name</th>
          <th>Address</th>
          <th>Phone</th>
          <th>Age</th>
          <th>Email</th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let c of store.contacts">
          <td>{{ c.firstName }}</td>
          <td>{{ c.lastName }}</td>
          <td>
            {{ c.addressLineOne }}, {{ c.city }},
            {{ c.country }}
          </td>
          <td>{{ c.phone }}</td>
          <td>{{ c.age }}</td>
          <td>{{ c.email }}</td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="openEditModal(editTemplate, c)"
            >
              Edit
            </button>
          </td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="deleteContact(c.id)"
            >
              Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

<ng-template #addTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Add Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="false"
    ></app-contact-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Edit Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="true"
      [contact]='selectedContact'
    ></app-contact-form>
  </div>
</ng-template>

We have a table to display the list of contacts. *ngFor=”let c of store.contacts” directive is in the tr tag to loop through the contacts stored in our MobX store.

The ng-template components are modals provided by ngx-bootstrap . We have addTemplate for using the contact form to add contact, and editTemplate to display the same form for editing. The tables and buttons are provided by regular Bootstrap

In home-page.component.scss , we add:

.home-page {
    padding: 20px;
}

.table-container {
    margin-top: 20px;
}

to add some padding and margin.

In app-routing.module.ts , which we get by choosing to including routing when running the Angular CLI wizard, we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

to let users see the home page when people navigate to the page by typing in the URL or clicking on the link.

In app.component.html , we replace the existing code with:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Address Book</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home </a>
      </li>
    </ul>
  </div>
</nav>
<router-outlet></router-outlet>

Here we add the Bootstrap navigation bar for showing the app title and navigation. The nav element is provided by plain Bootstrap.

Next in app.module.ts , we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ContactFormComponent } from './contact-form/contact-form.component';
import { ModalModule } from 'ngx-bootstrap/modal';
import { CustomFormsModule } from 'ng2-validation'
import { MobxAngularModule } from 'mobx-angular';
import { ContactsService } from './contacts.service';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    ContactFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    CustomFormsModule,
    ModalModule.forRoot(),
    MobxAngularModule
  ],
  providers: [
    ContactsService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

to include all the libraries we use for building the app along with the code we generated.

Then we create the MobX store. In contact-store.ts , we add:

import { observable, action } from 'mobx-angular';

class ContactStore {
    @observable contacts = [];
    @action setContacts(contacts) {
        this.contacts = contacts;
    }
}

export const contactStore = new ContactStore();

The store is very simple. It has the contacts array which we decorate with the observable decorator so that it will be always updated everywhere when we reference it in our code. The setContacts function let us set the value the contacts array which will be propagated to any code that references it since we have the observable decorator in front of it. The action decorator means that we designate the function to be able to update observable values in the store.

We export the instance of the store, which we used our components wherever we referenced contactStore .

Next in contact.service.ts , we add:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';

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

  constructor(
    private http: HttpClient
  ) { }

  getContacts() {
    return this.http.get(`${environment.apiUrl}/contacts`);
  }

  addContact(data) {
    return this.http.post(`${environment.apiUrl}/contacts`, data);
  }

  editContact(data) {
    return this.http.put(`${environment.apiUrl}/contacts/${data.id}`, data);
  }

  deleteContact(id) {
    return this.http.delete(`${environment.apiUrl}/contacts/${id}`);
  }
}

to let us make HTTP requests to our back end for manipulating contacts.

Next in exports.ts , we add:

export const COUNTRIES = [
    { "name": "Afghanistan", "code": "AF" },
    { "name": "Aland Islands", "code": "AX" },
    { "name": "Albania", "code": "AL" },
    { "name": "Algeria", "code": "DZ" },
    { "name": "American Samoa", "code": "AS" },
    { "name": "AndorrA", "code": "AD" },
    { "name": "Angola", "code": "AO" },
    { "name": "Anguilla", "code": "AI" },
    { "name": "Antarctica", "code": "AQ" },
    { "name": "Antigua and Barbuda", "code": "AG" },
    { "name": "Argentina", "code": "AR" },
    { "name": "Armenia", "code": "AM" },
    { "name": "Aruba", "code": "AW" },
    { "name": "Australia", "code": "AU" },
    { "name": "Austria", "code": "AT" },
    { "name": "Azerbaijan", "code": "AZ" },
    { "name": "Bahamas", "code": "BS" },
    { "name": "Bahrain", "code": "BH" },
    { "name": "Bangladesh", "code": "BD" },
    { "name": "Barbados", "code": "BB" },
    { "name": "Belarus", "code": "BY" },
    { "name": "Belgium", "code": "BE" },
    { "name": "Belize", "code": "BZ" },
    { "name": "Benin", "code": "BJ" },
    { "name": "Bermuda", "code": "BM" },
    { "name": "Bhutan", "code": "BT" },
    { "name": "Bolivia", "code": "BO" },
    { "name": "Bosnia and Herzegovina", "code": "BA" },
    { "name": "Botswana", "code": "BW" },
    { "name": "Bouvet Island", "code": "BV" },
    { "name": "Brazil", "code": "BR" },
    { "name": "British Indian Ocean Territory", "code": "IO" },
    { "name": "Brunei Darussalam", "code": "BN" },
    { "name": "Bulgaria", "code": "BG" },
    { "name": "Burkina Faso", "code": "BF" },
    { "name": "Burundi", "code": "BI" },
    { "name": "Cambodia", "code": "KH" },
    { "name": "Cameroon", "code": "CM" },
    { "name": "Canada", "code": "CA" },
    { "name": "Cape Verde", "code": "CV" },
    { "name": "Cayman Islands", "code": "KY" },
    { "name": "Central African Republic", "code": "CF" },
    { "name": "Chad", "code": "TD" },
    { "name": "Chile", "code": "CL" },
    { "name": "China", "code": "CN" },
    { "name": "Christmas Island", "code": "CX" },
    { "name": "Cocos (Keeling) Islands", "code": "CC" },
    { "name": "Colombia", "code": "CO" },
    { "name": "Comoros", "code": "KM" },
    { "name": "Congo", "code": "CG" },
    { "name": "Congo, The Democratic Republic of the", "code": "CD" },
    { "name": "Cook Islands", "code": "CK" },
    { "name": "Costa Rica", "code": "CR" },
    {
        "name": "Cote D"Ivoire", "code": "CI"
    },
    { "name": "Croatia", "code": "HR" },
    { "name": "Cuba", "code": "CU" },
    { "name": "Cyprus", "code": "CY" },
    { "name": "Czech Republic", "code": "CZ" },
    { "name": "Denmark", "code": "DK" },
    { "name": "Djibouti", "code": "DJ" },
    { "name": "Dominica", "code": "DM" },
    { "name": "Dominican Republic", "code": "DO" },
    { "name": "Ecuador", "code": "EC" },
    { "name": "Egypt", "code": "EG" },
    { "name": "El Salvador", "code": "SV" },
    { "name": "Equatorial Guinea", "code": "GQ" },
    { "name": "Eritrea", "code": "ER" },
    { "name": "Estonia", "code": "EE" },
    { "name": "Ethiopia", "code": "ET" },
    { "name": "Falkland Islands (Malvinas)", "code": "FK" },
    { "name": "Faroe Islands", "code": "FO" },
    { "name": "Fiji", "code": "FJ" },
    { "name": "Finland", "code": "FI" },
    { "name": "France", "code": "FR" },
    { "name": "French Guiana", "code": "GF" },
    { "name": "French Polynesia", "code": "PF" },
    { "name": "French Southern Territories", "code": "TF" },
    { "name": "Gabon", "code": "GA" },
    { "name": "Gambia", "code": "GM" },
    { "name": "Georgia", "code": "GE" },
    { "name": "Germany", "code": "DE" },
    { "name": "Ghana", "code": "GH" },
    { "name": "Gibraltar", "code": "GI" },
    { "name": "Greece", "code": "GR" },
    { "name": "Greenland", "code": "GL" },
    { "name": "Grenada", "code": "GD" },
    { "name": "Guadeloupe", "code": "GP" },
    { "name": "Guam", "code": "GU" },
    { "name": "Guatemala", "code": "GT" },
    { "name": "Guernsey", "code": "GG" },
    { "name": "Guinea", "code": "GN" },
    { "name": "Guinea-Bissau", "code": "GW" },
    { "name": "Guyana", "code": "GY" },
    { "name": "Haiti", "code": "HT" },
    { "name": "Heard Island and Mcdonald Islands", "code": "HM" },
    { "name": "Holy See (Vatican City State)", "code": "VA" },
    { "name": "Honduras", "code": "HN" },
    { "name": "Hong Kong", "code": "HK" },
    { "name": "Hungary", "code": "HU" },
    { "name": "Iceland", "code": "IS" },
    { "name": "India", "code": "IN" },
    { "name": "Indonesia", "code": "ID" },
    { "name": "Iran, Islamic Republic Of", "code": "IR" },
    { "name": "Iraq", "code": "IQ" },
    { "name": "Ireland", "code": "IE" },
    { "name": "Isle of Man", "code": "IM" },
    { "name": "Israel", "code": "IL" },
    { "name": "Italy", "code": "IT" },
    { "name": "Jamaica", "code": "JM" },
    { "name": "Japan", "code": "JP" },
    { "name": "Jersey", "code": "JE" },
    { "name": "Jordan", "code": "JO" },
    { "name": "Kazakhstan", "code": "KZ" },
    { "name": "Kenya", "code": "KE" },
    { "name": "Kiribati", "code": "KI" },
    {
        "name": "Korea, Democratic People"S Republic of", "code": "KP"
    },
    { "name": "Korea, Republic of", "code": "KR" },
    { "name": "Kuwait", "code": "KW" },
    { "name": "Kyrgyzstan", "code": "KG" },
    {
        "name": "Lao People"S Democratic Republic", "code": "LA"
    },
    { "name": "Latvia", "code": "LV" },
    { "name": "Lebanon", "code": "LB" },
    { "name": "Lesotho", "code": "LS" },
    { "name": "Liberia", "code": "LR" },
    { "name": "Libyan Arab Jamahiriya", "code": "LY" },
    { "name": "Liechtenstein", "code": "LI" },
    { "name": "Lithuania", "code": "LT" },
    { "name": "Luxembourg", "code": "LU" },
    { "name": "Macao", "code": "MO" },
    { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },
    { "name": "Madagascar", "code": "MG" },
    { "name": "Malawi", "code": "MW" },
    { "name": "Malaysia", "code": "MY" },
    { "name": "Maldives", "code": "MV" },
    { "name": "Mali", "code": "ML" },
    { "name": "Malta", "code": "MT" },
    { "name": "Marshall Islands", "code": "MH" },
    { "name": "Martinique", "code": "MQ" },
    { "name": "Mauritania", "code": "MR" },
    { "name": "Mauritius", "code": "MU" },
    { "name": "Mayotte", "code": "YT" },
    { "name": "Mexico", "code": "MX" },
    { "name": "Micronesia, Federated States of", "code": "FM" },
    { "name": "Moldova, Republic of", "code": "MD" },
    { "name": "Monaco", "code": "MC" },
    { "name": "Mongolia", "code": "MN" },
    { "name": "Montenegro", "code": "ME" },
    { "name": "Montserrat", "code": "MS" },
    { "name": "Morocco", "code": "MA" },
    { "name": "Mozambique", "code": "MZ" },
    { "name": "Myanmar", "code": "MM" },
    { "name": "Namibia", "code": "NA" },
    { "name": "Nauru", "code": "NR" },
    { "name": "Nepal", "code": "NP" },
    { "name": "Netherlands", "code": "NL" },
    { "name": "Netherlands Antilles", "code": "AN" },
    { "name": "New Caledonia", "code": "NC" },
    { "name": "New Zealand", "code": "NZ" },
    { "name": "Nicaragua", "code": "NI" },
    { "name": "Niger", "code": "NE" },
    { "name": "Nigeria", "code": "NG" },
    { "name": "Niue", "code": "NU" },
    { "name": "Norfolk Island", "code": "NF" },
    { "name": "Northern Mariana Islands", "code": "MP" },
    { "name": "Norway", "code": "NO" },
    { "name": "Oman", "code": "OM" },
    { "name": "Pakistan", "code": "PK" },
    { "name": "Palau", "code": "PW" },
    { "name": "Palestinian Territory, Occupied", "code": "PS" },
    { "name": "Panama", "code": "PA" },
    { "name": "Papua New Guinea", "code": "PG" },
    { "name": "Paraguay", "code": "PY" },
    { "name": "Peru", "code": "PE" },
    { "name": "Philippines", "code": "PH" },
    { "name": "Pitcairn", "code": "PN" },
    { "name": "Poland", "code": "PL" },
    { "name": "Portugal", "code": "PT" },
    { "name": "Puerto Rico", "code": "PR" },
    { "name": "Qatar", "code": "QA" },
    { "name": "Reunion", "code": "RE" },
    { "name": "Romania", "code": "RO" },
    { "name": "Russian Federation", "code": "RU" },
    { "name": "RWANDA", "code": "RW" },
    { "name": "Saint Helena", "code": "SH" },
    { "name": "Saint Kitts and Nevis", "code": "KN" },
    { "name": "Saint Lucia", "code": "LC" },
    { "name": "Saint Pierre and Miquelon", "code": "PM" },
    { "name": "Saint Vincent and the Grenadines", "code": "VC" },
    { "name": "Samoa", "code": "WS" },
    { "name": "San Marino", "code": "SM" },
    { "name": "Sao Tome and Principe", "code": "ST" },
    { "name": "Saudi Arabia", "code": "SA" },
    { "name": "Senegal", "code": "SN" },
    { "name": "Serbia", "code": "RS" },
    { "name": "Seychelles", "code": "SC" },
    { "name": "Sierra Leone", "code": "SL" },
    { "name": "Singapore", "code": "SG" },
    { "name": "Slovakia", "code": "SK" },
    { "name": "Slovenia", "code": "SI" },
    { "name": "Solomon Islands", "code": "SB" },
    { "name": "Somalia", "code": "SO" },
    { "name": "South Africa", "code": "ZA" },
    { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },
    { "name": "Spain", "code": "ES" },
    { "name": "Sri Lanka", "code": "LK" },
    { "name": "Sudan", "code": "SD" },
    { "name": "Suriname", "code": "SR" },
    { "name": "Svalbard and Jan Mayen", "code": "SJ" },
    { "name": "Swaziland", "code": "SZ" },
    { "name": "Sweden", "code": "SE" },
    { "name": "Switzerland", "code": "CH" },
    { "name": "Syrian Arab Republic", "code": "SY" },
    { "name": "Taiwan, Province of China", "code": "TW" },
    { "name": "Tajikistan", "code": "TJ" },
    { "name": "Tanzania, United Republic of", "code": "TZ" },
    { "name": "Thailand", "code": "TH" },
    { "name": "Timor-Leste", "code": "TL" },
    { "name": "Togo", "code": "TG" },
    { "name": "Tokelau", "code": "TK" },
    { "name": "Tonga", "code": "TO" },
    { "name": "Trinidad and Tobago", "code": "TT" },
    { "name": "Tunisia", "code": "TN" },
    { "name": "Turkey", "code": "TR" },
    { "name": "Turkmenistan", "code": "TM" },
    { "name": "Turks and Caicos Islands", "code": "TC" },
    { "name": "Tuvalu", "code": "TV" },
    { "name": "Uganda", "code": "UG" },
    { "name": "Ukraine", "code": "UA" },
    { "name": "United Arab Emirates", "code": "AE" },
    { "name": "United Kingdom", "code": "GB" },
    { "name": "United States", "code": "US" },
    { "name": "United States Minor Outlying Islands", "code": "UM" },
    { "name": "Uruguay", "code": "UY" },
    { "name": "Uzbekistan", "code": "UZ" },
    { "name": "Vanuatu", "code": "VU" },
    { "name": "Venezuela", "code": "VE" },
    { "name": "Viet Nam", "code": "VN" },
    { "name": "Virgin Islands, British", "code": "VG" },
    { "name": "Virgin Islands, U.S.", "code": "VI" },
    { "name": "Wallis and Futuna", "code": "WF" },
    { "name": "Western Sahara", "code": "EH" },
    { "name": "Yemen", "code": "YE" },
    { "name": "Zambia", "code": "ZM" },
    { "name": "Zimbabwe", "code": "ZW" }
]

so that we have a list of countries for the Countries drop down in our contact form.

In environment.ts , we add:

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'
};

so that we can talk to our back end.

Then in index.html , we have:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Angular Address Book App</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

to change the default title to our own and added Bootstrap CSS.

To run our app, first we use JSON server package located at https://github.com/typicode/json-server to create a simple back end without writing any code. Run npm i -g json-server to install it.

We create a file called db.json in the root folder and add:

{
  "contacts": []
}

so that we get the routes that we specified in contacts.service.ts .

Then we run json-server --watch db.json to start our back end, and run ng serve on to start our app.

Categories
Angular

Add Tool Tips to Your Angular App to Help Users Use Your App

Tool tips are common for providing hints on how to use different parts of a web app. It is easy to add and it helps users understand the app more. They’re also useful for display long text that would be too long.

Ngx-Bootstrap has tool tips built in as a component. It also has many other components that we can use. Adding tool tips using Ngx-Bootstrap is easy. See https://valor-software.com/ngx-bootstrap/#/tooltip for a full set of options for the tool tip. It can have text, position can change, also can change position according to screen size changes. The content can also be HTML. Trigger for tool tips can also be customized.

In this article, we will write an employee manager app that lets users enter their employee data in a form. Users can add, edit and delete their data with tool tips to guide them through the forms when they use the form.

To start the project, we install the Angular CLI if not installed already by running npm i -g @angular/cli . Next we run the Angular CLI to create the project by typing:

ng new employee-manager

In the wizard, we choose to include routing and use SCSS as our CSS preprocessor.

Then we install some packages. We need the Ngx-Bootstrap for styling, modal, and the tool tips, as well as MobX to store the countries in a shared store. To install them, we run:

npm i ngx-bootatrap mobx mobx-angular

Next we create our components, classes and services. To do this, we run:

ng g component employeeForm
ng g component homePage
ng g service employees
ng g class employeeStore
ng g class employee

Now we are ready to write some code. In employee-form.component.html, we replace the existing code with:

<form (ngSubmit)="save(employeeForm)" #employeeForm="ngForm">
  <div class="form-group">
    <label tooltip="Enter employee's name here" placement="right">Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="Name"
      #name="ngModel"
      name="name"
      [(ngModel)]="form.name"
      required
    />
    <div *ngIf="name?.invalid && (name.dirty || name.touched)">
      <div *ngIf="name.errors.required">
        Name is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Enter employee's address here" placement="right"
      >Address</label
    >
    <input
      type="text"
      class="form-control"
      placeholder="Address"
      #address="ngModel"
      name="address"
      [(ngModel)]="form.address"
      required
    />
    <div *ngIf="address?.invalid && (address.dirty || address.touched)">
      <div *ngIf="address.errors.required">
        Address is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Enter employee's position here" placement="right"
      >Position</label
    >
    <input
      type="text"
      class="form-control"
      placeholder="Position"
      #position="ngModel"
      name="position"
      [(ngModel)]="form.position"
      required
    />
    <div *ngIf="position?.invalid && (position.dirty || position.touched)">
      <div *ngIf="position.errors.required">
        Position is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Select employee's employment status" placement="right"
      >Employment Status</label
    >
    <select
      class="form-control"
      #status="ngModel"
      name="status"
      [(ngModel)]="form.status"
      required
    >
      <option value="Employed">Employed</option>
      <option value="Terminated">Terminated</option>
    </select>
    <div *ngIf="status?.invalid && (status.dirty || status.touched)">
      <div *ngIf="status.errors.required">
        Status is required.
      </div>
    </div>
  </div>

  <button class="btn btn-primary">Save</button>
</form>

This is the template for the employee form for letting users add and edit employee data. We have name, address, position, and status fields, and they’re all set as required. Error messages are displayed when any of them aren’t filled in and submitted will be stopped if the user tries to submit without any of these data. The labels will show the tool tips when users hover over them. We set the placementto right so that they display on the right. When users click the Save button the save function in the code is called.

Then in employee-form.component.ts , we replace the existing code with:

import { Component, OnInit, Output, Input, SimpleChanges, EventEmitter } from '@angular/core';
import { employeeStore } from '../employee-store';
import { EmployeesService } from '../employees.service';
import { NgForm } from '@angular/forms';
import { Employee } from '../employee';

@Component({
  selector: 'app-employee-form',
  templateUrl: './employee-form.component.html',
  styleUrls: ['./employee-form.component.scss']
})
export class EmployeeFormComponent implements OnInit {
  form: Employee = <Employee>{};
  @Output('saved') saved = new EventEmitter();
  @Input() edit: boolean;
  @Input() selectedEmployee: Employee;
  store = employeeStore;

  constructor(private employeeService: EmployeesService) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    this.form = Object.assign({}, this.selectedEmployee);
  }

  save(employeeForm: NgForm) {
    if (employeeForm.invalid) {
      return;
    }
    if (this.edit) {
      this.employeeService.editEmployee(this.form)
        .subscribe(res => {
          this.getEmployees()
          this.saved.emit();
        })
    }
    else {
      this.employeeService.addEmployee(this.form)
        .subscribe(res => {
          this.getEmployees()
          this.saved.emit();
        })
    }
  }

  getEmployees() {
    this.employeeService.getEmployees()
      .subscribe((res: Employee[]) => {
        this.store.setEmployees(res);
      })
  }

}

This file has the functions that we called in the previous template like the save function. We also have the Inputs to get the data from the home page, as well as an Output to emit a saved event to the home page. Since we use the form for editing, we also need to pass in the selected entry with the selectedEmployee Input, so that it can be edited. To update the form object with the selectedEmployee values, we copied the value whenever the selectedEmployee Input changes.

In the save function, we validate the form and call different functions for saving depending on whether the form is being used for adding or editing an entry. The latest entries will be populated in our MobX store by calling the getEmployees function and the saved event will be emitted once that’s done.

Next in home-page.component.html , we replace the code with:

<ng-template #addTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Add Employee</h2>
  </div>
  <div class="modal-body">
    <app-employee-form (saved)="closeModals()"></app-employee-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Edit Employee</h2>
  </div>
  <div class="modal-body">
    <app-employee-form
      [edit]="true"
      (saved)="closeModals()"
      [selectedEmployee]="selectedEmployee"
    ></app-employee-form>
  </div>
</ng-template>

<h1 class="text-center">Employee Manager</h1>

<div class="btn-group" role="group" id="add-group">
  <button
    type="button"
    class="btn btn-secondary"
    (click)="openAddModal(addTemplate)"
  >
    Add Employee
  </button>
</div>

<br />

<div class="card" *ngFor="let e of store.employees">
  <div class="card-body">
    <h5 class="card-title">{{ e.name }}</h5>
    <p class="card-text">Position: {{ e.position }}</p>
    <p class="card-text">Address: {{ e.address }}</p>
    <p class="card-text">Employment Status: {{ e.status }}</p>
    <button (click)="openEditModal(editTemplate, e)" class="btn btn-primary">
      Edit
    </button>
    <button (click)="deleteEmployee(e.id)" class="btn btn-primary">
      Delete
    </button>
  </div>
</div>

to add buttons for adding, editing, and deleting entries. The employee entries are displayed in the cards. The Edit and Delete buttons are displayed at the bottom of the cards. Also, we have the modals for adding and editing entries that we open with the Add and Edit buttons respectively.

In home-page.component.scss , we add:

$margin: 10px;

#add-group {
  margin-bottom: $margin;
}

button {
  margin-right: $margin;
}

to add some margins to our buttons.

Next in home-page.component.ts , we replace the existing code with:

import { Component, OnInit, TemplateRef } from '@angular/core';
import { employeeStore } from '../employee-store';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { EmployeesService } from '../employees.service';
import { Employee } from '../employee';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  addModalRef: BsModalRef;
  editModalRef: BsModalRef;
  selectedEmployee: Employee = <Employee>{};
  store = employeeStore;

  constructor(
    private modalService: BsModalService,
    private employeeService: EmployeesService
  ) { }

  ngOnInit() {
    this.getEmployees()
  }

  getEmployees() {
    this.employeeService.getEmployees()
      .subscribe((res: Employee[]) => {
        this.store.setEmployees(res);
      })
  }

  openAddModal(template: TemplateRef<any>) {
    this.addModalRef = this.modalService.show(template);
  }

  openEditModal(template: TemplateRef<any>, employee: Employee) {
    this.editModalRef = this.modalService.show(template);
    this.selectedEmployee = employee;
  }

  closeModals() {
    this.addModalRef && this.addModalRef.hide();
    this.editModalRef && this.editModalRef.hide();
  }

  deleteEmployee(id) {
    this.employeeService.deleteEmployee(id)
      .subscribe((res: Employee[]) => {
        this.getEmployees();
      })
  }
}

We have the openAddModal and openEditModal functions to open the Add and Edit modals. The closeModals function is for closing the modals when things are saved in the app-employee-form component. The deleteEmployee function is for deleting the employee, and the getEmployees is used for getting the entries when the page loads when items are deleted and out the items in our store so every component can access it.

In app-routing.module.ts , we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

so users can see the pages we just added when they click on the links or enter the URLs.

Next in app.component.html , we put:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" routerLink="/">Employee Manager</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" routerLink="/">Home </a>
      </li>
    </ul>
  </div>
</nav>

<div class="page">
  <router-outlet></router-outlet>
</div>

to add the links to our pages and expose the router-outlet so users can see our pages.

Then in app.component.scss , we add:

.page {
  padding: 20px;
}

nav {
  background-color: aquamarine !important;
}

to add padding to our pages and change the color of our Bootstrap navigation bar.

In app.module.ts , we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { EmployeeFormComponent } from './employee-form/employee-form.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ModalModule } from 'ngx-bootstrap/modal';
import { EmployeesService } from './employees.service';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    EmployeeFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    ModalModule.forRoot(),
    TooltipModule.forRoot()
  ],
  providers: [
    EmployeesService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

we add our components, services, and libraries that we use in our app. Note that we add each component in Ngx-Bootstrap separately so we only add what we need.

In employee.ts , we add:

export class Employee {
    public id: number;
    public name: string;
    public address: string;
    public position: string;
    public status: string;
}

to add types to our employee form model.

Then in employeeStore.ts , we add:

import { observable, action } from 'mobx-angular';
import { Employee } from './employee';

class EmployeeStore {
    @observable employees: Employee[] = [];
    @action setEmployees(employees) {
        this.employees = employees;
    }
}

export const employeeStore = new EmployeeStore();

to create the MobX store to get our components share the data. Whenever we call this.store.setEmployees our components we set the currencies data in this store since we added the @action decorator before it. When we call this.store.employees in our component code we are always getting the latest value from this store since has the @observable decorator. We add the Employee[] type to employees so we don’t have to guess the fields available.

Then in employee.service.ts , we replace the existing code with:

import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient } from '@angular/common/http';

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

  constructor(private http: HttpClient) { }

  getEmployees() {
    return this.http.get(`${environment.apiUrl}/employees`);
  }

  addEmployee(data) {
    return this.http.post(`${environment.apiUrl}/employees`, data);
  }

  editEmployee(data) {
    return this.http.put(`${environment.apiUrl}/employees/${data.id}`, data);
  }

  deleteEmployee(id) {
    return this.http.delete(`${environment.apiUrl}/employees/${id}`);
  }

}

so that we can make HTTP requests to our back end to get, save and delete the user’s employee entries.

Next in environment.prod.ts and environment.ts , we replace the existing code with:

export const environment = {
  production: true,
  apiUrl: 'http://localhost:3000'
};

to add our API’s URL.

Finally, in index.html , we replace the code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Employee Manager</title>
    <base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
      integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

to add the Bootstrap CSS and JavaScript dependencies into our app, as well as changing the title.

After all the work, we can run ng serve to run the app.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "employees": [
  ]
}

So we have the employees endpoints defined in the requests.js available.

Categories
Angular Material

Angular Material — Nested Tree

Angular Material is a popular UI framework based on Material Design for Angular.

In this article, we’ll look at how to use Angular Material into our Angular project.

Nested Tree

We can add a tree with nesting withn the mat-nested-tree-node component.

For example, we can write:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTreeModule } from '@angular/material/tree';
import { MatButtonModule } from '@angular/material/button';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatTreeModule,
    MatButtonModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

import { NestedTreeControl } from '@angular/cdk/tree';
import { Component } from '@angular/core';
import { MatTreeNestedDataSource } from '@angular/material/tree';

interface FoodNode {
  name: string;
  children?: FoodNode[];
}

const TREE_DATA: FoodNode[] = [
  {
    name: 'Fruit',
    children: [
      { name: 'Apple' },
      { name: 'Banana' },
      { name: 'Fruit loops' },
    ]
  }, {
    name: 'Vegetables',
    children: [
      {
        name: 'Green',
        children: [
          { name: 'Broccoli' },
          { name: 'Brussels sprouts' },
        ]
      }, {
        name: 'Orange',
        children: [
          { name: 'Pumpkins' },
          { name: 'Carrots' },
        ]
      },
    ]
  },
];

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  treeControl = new NestedTreeControl<FoodNode>(node => node.children);
  dataSource = new MatTreeNestedDataSource<FoodNode>();

  constructor() {
    this.dataSource.data = TREE_DATA;
  }

  hasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
}

app.component.html

<div>
  <mat-tree [dataSource]="dataSource" [treeControl]="treeControl"
    class="example-tree">
    <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
      <li class="mat-tree-node">
        <button mat-icon-button disabled></button>
        {{node.name}}
      </li>
    </mat-tree-node>
    <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
      <li>
        <div class="mat-tree-node">
          <button mat-icon-button matTreeNodeToggle>
            <mat-icon class="mat-icon-rtl-mirror">
              {{treeControl.isExpanded(node) ? '&#8595; ' : '&#8594;'}}
            </mat-icon>
          </button>
          {{node.name}}
        </div>
        <ul [class.example-tree-invisible]="!treeControl.isExpanded(node)">
          <ng-container matTreeNodeOutlet></ng-container>
        </ul>
      </li>
    </mat-nested-tree-node>
  </mat-tree>
</div>

styles.css

.example-tree-invisible {
  display: none;
}

.example-tree ul,
.example-tree li {
  margin-top: 0;
  margin-bottom: 0;
  list-style-type: none;
}

We add the MatTreeModule to let us add the tree nodes.

The MatButtonModule lets us add the buttons for toggling the child nodes.

In app.component.ts , we have a nested array with the nested data.

Then we pass that into the MatTreeNestedDataSource constructor to create the data.

We also have to create the NestedTreecontrol instance and bind that to the mat-tree .

The hasChild method is also needed to determine if a node has child nodes.

In the app.component.html file, we show the mat-tree with the mat-tree-node to show flat tree nodes.

And the mat-nested-tree-node shows the nested tree node.

the *matTreeNodeDef are different for each.

The treeControl.isExpanded method lets us check whether the node is expanded and show content accordingly.

Conclusion

We can add a nested tree with Angular Material.