Categories
Angular Answers

How to share the result of an Angular Http network call in Rxjs 5?

Sometimes, we want to share the result of an Angular Http network call in Rxjs 5.

In this article, we’ll look at how to share the result of an Angular Http network call in Rxjs 5.

How to share the result of an Angular Http network call in Rxjs 5?

To share the result of an Angular Http network call in Rxjs 5, we can create our own service.

For instance, we write

@Injectable()
export class HttpCache {
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    let cached: any;
    if (cached === sessionStorage.getItem(url)) {
      return Observable.of(JSON.parse(cached));
    } else {
      return this.http.get(url).map((resp) => {
        sessionStorage.setItem(url, resp.text());
        return resp.json();
      });
    }
  }
}

to create the HttpCache service class that has the get method.

In get, we check if we stored the response in session storage.

If it is, then we return an observable that has the cached result.

Otherwise, we call this.http.get to make a GET request to the url.

And we call sessionStorage.setItem to store the response once we have it.

Conclusion

To share the result of an Angular Http network call in Rxjs 5, we can create our own service.

Categories
Angular Answers

How to create dynamic template to compile dynamic component with Angular?

Sometimes, we want to create dynamic template to compile dynamic component with Angular.

In this article, we’ll look at how to create dynamic template to compile dynamic component with Angular.

How to create dynamic template to compile dynamic component with Angular?

To create dynamic template to compile dynamic component with Angular, we can use angular-elements.

To install it, we run

npm i @angular/elements

Then we create a service with

import { Injectable, Injector } from "@angular/core";
import { createCustomElement } from "@angular/elements";
import { IStringAnyMap } from "src/app/core/models";
import { AppUserIconComponent } from "src/app/shared";

const COMPONENTS = {
  "user-icon": AppUserIconComponent,
};

@Injectable({
  providedIn: "root",
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {}

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, {
        injector: this.injector,
      });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

In it, we have the register method that registers the custom components in components.

And then we add the create method to return the custom elements after creating it.

Next, we register our custom component in the component so we can use it within AppComponent.

@Component({
  selector: "app-root",
  template: "<router-outlet></router-outlet>",
})
export class AppComponent {
  constructor(dynamicComponents: DynamicComponentsService) {
    dynamicComponents.register();
  }
}

We inject the dynamicComponents service and call register to use itin AppComponent.

Then we create the dynamic component with

dynamicComponents.create("user-icon", {
  user: {
    //...
  },
});

And then we can use it in our code with

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;
this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

in our component code and with

<div class="comment-item d-flex" [innerHTML]="content"></div>

in the template of the component.

Conclusion

To create dynamic template to compile dynamic component with Angular, we can use angular-elements.

Categories
Angular Answers

How to bind HTML with Angular?

Sometimes, we want to bind HTML with Angular.

In this article, we’ll look at how to bind HTML with Angular.

How to bind HTML with Angular?

To bind HTML with Angular, we use [innerHTML].

For instance, we write

<div [innerHTML]="htmlString"></div>

to render the contents of htmlString as raw HTML.

We use [innerHTML] to make render htmlString as is instead of escaping the string before rendering it.

Conclusion

To bind HTML with Angular, we use [innerHTML].

Categories
Angular

How to Use Input Masks to Validate Input in an Angular App

Input validation is always a chore to set up. An input mask is a way to enforce the format of the user’s input in a simple way. When an input mask is applied to an input element, only input in a set format can be entered.

For example, if an input has an input mask of for phone may have 3 digits for the area code, followed by a dash, then 3 digits for the prefix, followed by another dash, and then followed by the remaining 4 digits.

There are many JavaScript libraries for adding an input task to input fields. If we are writing an Angular app, we can use the ngx-mask library, located at https://www.npmjs.com/package/ngx-mask. The library allows us to enforce input format and also has a pipe to transform template variables to a designated format.

In this article, we will build a tip calculator that gets the tip rates of different countries and let users enter the amount they’ve spent before tips, and the number of people to split the after tip amount with. We will get the list of countries along with their currencies from https://restcountries.eu/rest/v2/all. To start building the app, we first install Angular CLI if not installed already by running npm i -g @angular/cli . Next, we create our project by running ng new tip-calculator . In the wizard, we choose to include routing and use SCSS as our CSS preprocessor.

Then we install some packages. We need the ngx-mask package we mentioned above as well as MobX to store the countries in a shared store. To install them, we run:

npm i ngx-mask mobx mobx-angular

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

ng g component homePage
ng g service tips
ng g class countriesStore

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

<h1 class="text-center">Tip Calculator</h1>
<form (ngSubmit)="calculate(tipForm)" #tipForm="ngForm">
  <div class="form-group">
    <label>Amount</label>
    <input
      type="text"
      class="form-control"
      placeholder="Amount"
      #amount="ngModel"
      name="amount"
      [(ngModel)]="form.amount"
      required
      mask="9999990*.99"
    />
    <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>

  <div class="form-group">
    <label>Number of People</label>
    <input
      type="text"
      class="form-control"
      placeholder="Number of People"
      #amount="ngModel"
      name="amount"
      [(ngModel)]="form.numPeople"
      required
      mask="9999990"
    />
    <div *ngIf="numPeople?.invalid && (numPeople.dirty || numPeople.touched)">
      <div *ngIf="numPeople.errors.required">
        Number of people is required.
      </div>
      <div *ngIf="numPeople.invalid">
        Number of people is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Country</label>
    <select
      class="form-control"
      #country="ngModel"
      name="country"
      [(ngModel)]="form.country"
      required
    >
      <option *ngFor="let c of store.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>

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

<br />

<div class="card">
  <div class="card-body">
    <h5 class="card-title">Result</h5>
    <p class="card-text">
      Amount after tip: {{ amountAfterTip | mask: "9999999.99" }} {{ currency }}
    </p>
    <p class="card-text">
      Amount after tip split between {{ this.form.numPeople }} people:
      {{ splitAmountAfterTip | mask: "9999999.99" }} {{ currency }}
    </p>
  </div>
</div>

We add the tip calculator form to let users enter their before tip amount, the number of people eating together, and the country the user is in. We use Angular’s template driven form validation to check if everything is filled in. In addition, we use the mask directive provided by ngx-mask to make sure that users enter a monetary amount into the Amount field, and we use the same directive to enforce the that the number of people is a non-negative number. The Country field has options populated by getting them from our MobX store.

At the bottom of the page, we display the results after calculation. We use the mask filter also provided by ngx-mask to display the currency amounts with the right amount of decimal places.

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

import { Component, OnInit } from '@angular/core';
import { TipsService } from '../tips.service';
import { countriesStore } from '../countries-store';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  store = countriesStore;
  countries: any[] = [];
  form: any = <any>{};
  amountAfterTip: number = 0;
  splitAmountAfterTip: number = 0;
  currency: string = '';

  constructor(private tipsService: TipsService) { }

  ngOnInit() {
    if (this.store.countries.length == 0) {
      this.tipsService.getCountries()
        .subscribe(res => {
          this.store.setCountries(res);
        })
    }
  }

  calculate(tipForm: NgForm) {
    if (tipForm.invalid) {
      return;
    }
    const country = this.store.countries.find(c => c.name == this.form.country)
    this.currency = country ? country.currencies[0].code : '';
    this.amountAfterTip = +this.form.amount * (1 + this.tipsService.getTipRates(this.form.country));
    this.splitAmountAfterTip = +this.amountAfterTip / +this.form.numPeople;
  }
}

In the ngOnInit hook, we get the countries and put them in our MobX store if it’s not populated already. We also have the calculate function that we called in the template. We check that the tipForm we defined in the template is valid, and if it is, then we do the tip calculations and get the currency name by the country.

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="/">Tip Calculator</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: lightsalmon !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 { NgxMaskModule } from 'ngx-mask'
import { MobxAngularModule } from 'mobx-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { TipsService } from './tips.service';

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

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

Then in countriesStore.ts , we add:

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

class CountriesStore {
    @observable countries = [];
    @action setCountries(countries) {
        this.countries = countries;
    }
}

export const countriesStore = new CountriesStore();

to create the MobX store to get our components share the data. Whenever we call this.store.setCountriesin our components we set the currencies data in this store since we added the @action decorator before it. When we call this.store.countries in our component code we are always getting the latest value from this store since has the @observable decorator.

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

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

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

  constructor(private http: HttpClient) { }

  getCountries() {
    return this.http.get('https://restcountries.eu/rest/v2/all');
  }

  getTipRates(country) {
    const ROUND_UP_COUNTRIES = `France,Italy,Hungary,Greece,Latvia`.split(',').map(c => c.trim());
    const FIVE_PERCENT_TIP_COUNTRIES = `
      Ecuador,
      Argentina,
      Austria,
      Albania,
      Turkey,
      India,
      Slovenia,
      Romania,
      Lithuania,
      Russia
    `.split(',')
      .map(c => c.trim());
    const TEN_PERCENT_TIP_COUNTRIES = `
      Colombia,
      Slovakia,
      Estonia,
      Cuba,
      Uruguay,
      Bulgaria
    `.split(',')
      .map(c => c.trim());
    const FIFTEEN_PERCENT_TIP_COUNTRIES = `
      Serbia,
      Canada,
      Mexico,
      Chile,
      Poland,
      Ukraine,
      Egypt,
      Armenia
    `.split(',')
      .map(c => c.trim());

    const TWENTY_PERCENT_TIP_COUNTRIES = ['United States']

    if (TWENTY_PERCENT_TIP_COUNTRIES.includes(country)) {
      return 0.2;
    }

    if (FIFTEEN_PERCENT_TIP_COUNTRIES.includes(country)) {
      return 0.15;
    }

    if (TEN_PERCENT_TIP_COUNTRIES.includes(country)) {
      return 0.1;
    }

    if (FIVE_PERCENT_TIP_COUNTRIES.includes(country) || ROUND_UP_COUNTRIES.includes(country)) {
      return 0.05;
    }

    return 0

  }
}

to add the getCountries function to get the countries list from the REST Countries API, and the getTipRates to get the tip rate by country.

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>World Tip Calculator</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"
      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. Then we should get:

You shouldn’t be able to enter anything other than numbers in the Amount and Number of People fields.

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.