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
JavaScript

Using the Destructuring Assignment Syntax in JavaScript

The destructuring assignment syntax is a JavaScript syntax feature that was introduced in the 2015 version of JavaScript and lets us unpack a list of values of an array or key-value pairs of an object into individual variables.

It’s very handy for retrieving entries from arrays or objects and setting them as values of individual variables. This is very handy because the alternative was to get an entry from an array from an index and then setting them as values of variables for arrays.

For objects, we have the value from the key and set them as values of variables.

Array Destructuring

We can use the destructuring assignment syntax easily in our code. For arrays, we can write:

const [a,b] = [1,2];

Then, we get 1 as the value of a and 2 as the value of b because the destructing syntax unpacked the entries of an array into individual variables.

Note that the number of items in the array does not have to equal the number of variables. For example, we can write:

const [a,b] = [1,2,3]

Then a is still 1 and b is still 2 because the syntax only sets the variables that are listed in the same order as the numbers appeared in the array. So, 1 is set to a, 2 is set to b, and 3 is ignored.

We can also use the rest operator to get the remaining variables that weren’t set to variables. For example, we can have:

const [a,b,...rest] = [1,2,3,4,5,6]

Then, rest would be [3,4,5,6] while we have a set to 1 and b set to 2. This lets us get the remaining array entries into a variable without setting them all to their own variables.

We can use the destructuring assignment syntax for objects as well. For example, we can write:

const {a,b} = {a:1, b:2};

In the code above, a is set to 1 and b is set to 2 as the key is matched to the name of the variable when assigning the values to variables.

Because we have a as the key and 1 as the corresponding value, the variable a is set to 1 as the key name matches the variable name. It is the same with b. We have a key named b with a value of 2, because we have the variable named b, we can set b to 2.

We can declare variables before assigning them with values with the destructuring assignment syntax. For example, we can write:

let a, b;
([a, b] = [1, 2]);

Then, we have a set to 1 and b set to 2 because a and b that were declared are the same ones that are assigned.

As long as the variable names are the same, the JavaScript interpreter is smart enough to do the assignment regardless of whether they’re declared beforehand or not.

We need the parentheses on the line so that the assignment will be interpreted as one line and not individual blocks with an equal sign in between, because two blocks on the same line aren’t valid syntax.

This is only required when the variable declarations happen before the destructuring assignment is made.

We can also set default values for destructuring assignments. For instance:

let a,b;
([a=1,b=2] = [0])

This is valid syntax. In the code above, we get that a is 0 because we assigned 0 to it. b is 2 because we didn’t assign anything to it.

The destructuring assignment syntax can also be used for swapping variables, so we can write:

let a = 1;
let b = 2;
([a,b] = [b,a])

b would become 1 and a would become 2 after the last line of the code above. We no longer have to assign things to temporary variables to swap them, and we also don’t have to add or subtract things to assign variables.

The destructuring assignment syntax also works for assigning returned values of a function to variables.

So, if a function returns an array or object, we can assign them to variables with the destructuring assignment syntax. For example, if we have:

const fn = () =>[1,2]

We can write:

const [a,b] = fn();

To get 1 as a and 2 as b with the destructuring syntax because the returned array is assigned to variables with the syntax.

Similarly, for objects, we can write:

const fn = () => {a:1, b:2}
const {a,b} = fn();

We can ignore variables in the middle by skipping the variable name in the middle of the destructuring assignment. For example, we can write:

const fn = () => [1,2,3];
let [a,,b] = fn();

We get a with the value of 1 and b with the value of 3, skipping the middle value.

It’s important to know that if we use the rest operator with a destructuring assignment syntax, we cannot have a trailing comma on the left side, so this:

let [a, ...b,] = [1, 2, 3];

Will result in a SyntaxError.

Object Destructuring

We can use the destructuring assignment syntax for objects as well. For example, we can write:

const {a,b} = {a:1, b:2};

In the code above, a is set to 1 and b is set to 2 because the key is matched to the name of the variable when assigning the values to variables.

As we have a as the key and 1 as the corresponding value, the variable a is set to 1 because the key name matches the variable name. It is the same with b. We have a key named b with a value of 2, because we have the variable named b, we can set b to 2.

We can also assign it to different variable names, so we don’t have to set the key-value entries to different variable names. We just have to add the name of the variable we want on the value part of the object on the left side, which is the one we want to assign it to, like the following:

const {a: foo, b: bar} = {a:1, b:2};

In the code above, we assigned the value of the key a to foo and the value of the key b to the variable bar. We still need a and b as the keys on the left side so they can be matched to the same key names on the right side for the destructuring assignment.

However, a and b aren’t actually defined as variables. It’s just used to match the key-value pairs on the right side so that they can be set to variables foo and bar.

Destructuring assignments with objects can also have default values. For example, we can write:

let {a = 1, b = 2} = {a: 3};

Then, we have a set to 3 and b set to 2 which is the default value as we didn’t have a key-value pair with a key named b on the right side.

Default values can also be provided if we use the destructuring syntax to assign values to variables that are named differently from the keys of the originating object. So, we can write:

const {a: foo=3, b: bar=4} = {a:1};

In this case, foo would be 1 and bar would be 4 because we assigned the left bar with the default value, but assigned foo to 1 with the destructuring assignment.

The destructuring assignment also works with nested objects. For example, if we have the following object:

let user = {
  id: 42,
  userName: 'dsmith',
  name: {
    firstName: 'Dave',
    lastName: 'Smith'
  }
};

We can write:

let {userName, name: { firstName }} = user;

To set displayName to 'dsmith' , and firstName to 'Dave'. The lookup is done for the whole object and, so, if the structure of the left object is the same as the right object and the keys exist, the destructuring assignment syntax will work.

We can also use the syntax for unpacking values into individual variables while passing objects in as arguments.

To do this, we put what we want to assign the values, which is the stuff that’s on the left side of the destructuring assignment expression, as the parameter of the function.

So, if we want to destructure user into its parts as variables, we can write a function like the following:

const who = ({userName, name: { firstName }}) => `${userName}'s first name is ${firstName}`;

who(user)

So, we get userName and firstName, which will be set as 'dsmith' and 'Dave' respectively as we applied the destructuring assignment syntax to the argument of the who function, which is the user object we defined before.

Likewise, we can set default parameters as with destructuring in parameters like we did with regular assignment expressions. So, we can write:

const who = ({userName = 'djones', name: { firstName }}) => `${userName}'s first name is ${firstName}`

If we have user set to:

let user = {
  id: 42,
  name: {
    firstName: 'Dave',
    lastName: 'Smith'
  }
};

Then we when call who(user), we get 'djones's first name is Dave' as we set 'djones' as the default value for userName.

We can use the destructuring assignment syntax when we are iterating through iterable objects. For example, we can write:

const people = [{
    firstName: 'Dave',
    lastName: 'Smith'
  },
  {
    firstName: 'Jane',
    lastName: 'Smith'
  },
  {
    firstName: 'Don',
    lastName: 'Smith'
  },
]

for (let {
  firstName,
  lastName
} of people) {
  console.log(firstName, lastName);
}

We get:

Dave Smith
Jane Smith
Don Smith

Logged, as the destructuring syntax works in for...of loops because the variable after the let is the entry of the array.

Computed object properties can also be on the left side of the destructuring assignment expressions. So, we can have something like:

let key = 'a';
let {[key]: bar} = {a: 1};

This will set bar to 1 because [key] is set to a and then the JavaScript interpreter can match the keys on both sides and do the destructuring assignment to the variable bar.

This also means that the key on the left side does not have to be a valid property or variable name. However, the variable name after the colon on the left side has to be a valid property or variable name.

For instance, we can write:

const obj = { 'abc 123': 1};
const { 'abc 123': abc123 } = obj;

console.log(abc123); // 1

As long as the key name is the same on both sides, we can have any key in a string to do a destructuring assignment to variables.

Another thing to note is that the destructuring assignment is smart enough to look for the keys on the same level of the prototype chain, so if we have:

var obj = {a: 1};
obj.__proto__.b = 2;
const {a, b} = obj;

We still get a set to 1 and b set to 2 as the JavaScript interpreter looks for b in the prototype inheritance chain and sets the values given by the key b.

As we can see, the destructuring assignment is a very powerful syntax. It saves lots of time writing code to assign array entries to variables or object values into their own variables.

It also lets us swap variables without temporary variables and makes the code much simpler and less confusing. It also works through inheritance so the property does not have to be in the object itself, but works even if the property is in its prototypes.

const fn = () => {a:1, b:2}
const {a,b} = fn();
Categories
React

How to Upload Files with React and Node.js

File upload is a common operation for web applications. In Node.js, with the Express web framework and the Multer library, adding file upload feature to your app is very easy.

To add file upload feature to your app, first you need an input field with type file. By default this type of input renders itself as a button which is difficult to style. The user clicks the dialog and if you attach an onChange handler to it, then you can access the file object by using JavaScript. The onChange handler takes a parameter which is the event object. The object has the file under the target.files property. Once you have that, you can add it to the FormData object with the append function to attach it to the form submission.

In this article, we will make a photo manager application that lets users enter a name, description, and upload a photo with the text.

We will use React for the frontend and Express with Multer for backend.

Backend: Node.js for File Upload

We start by building the backend. We create the project folder and a backend folder inside the project folder. Then go into the backend folder and run npx express-generator to generate the code files for our back end app.

Next, we run npm i to install the packages for our backend provided by the generator. We also need Babel to be able to use the import syntax, the CORS package for cross-domain communication, Multer for file upload with Express, Sequelize as our ORM, and SQLite3 as the database. We will use SQLite for simplicity.

To install all the packages, run npm i @babel/cli @babel/core @babel/node @babel/preset-env cors multer sequelize sqlite3 Then create a file called .babelrc in the root of the backend folder and add:

{
    "presets": [
        "@babel/preset-env]"
    ]
}

Then in the scripts section of package.json, we add:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

This lets us run our app with Babel instead of regular Node runtime. We should also install nodemon to watch for file changes and restart the app. Install it globally by running npm i -g nodemon.

Next we run npx sequelize-cli init in the backend folder to create the Sequelize ORM code to let us make and run migrations.

After that finishes, we should have a config.js created. In there, replace the existing code with:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

This specifies SQLite as our database.

Next we create a migration with the model:

npx sequelize-cli model:create --name Photo --attributes name:string,description:string,photoPath:string

Notice that we no spaces between commas after the attributes flag.

Run npx sequelize-cli db:migrate to create the database.

After that, we create the routes for manipulating photos. In the routes folder, create photos.js and add:

var express = require("express");
var multer = require("multer");
var router = express.Router();
const models = require("../models");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./uploads");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});

const upload = multer({
  storage
});

/* GET users listing. */
router.get("/", async (req, res, next) => {
  const photos = await models.Photo.findAll();
  res.json(photos);
});

router.post("/add", upload.single("photo"), async (req, res, next) => {
  try {
    const path = req.file.path;
    const { name, description } = req.body;
    const entry = await models.Photo.create({
      name,
      description,
      photoPath: path
    });
    res.json(entry);
  } catch (ex) {
    res.status(400).send({ error: ex });
  }
});

router.put("/edit", upload.single("photo"), async (req, res, next) => {
  try {
    const path = req.file && req.file.path;
    const { id, name, description } = req.body;
    let params = {};
    if (path) {
      params = {
        name,
        description,
        photoPath: path
      };
    } else {
      params = {
        name,
        description
      };
    }
    const photo = await models.Photo.update(params, {
      where: {
        id
      }
    });
    res.json(photo);
  } catch (ex) {
    res.status(400).send({ error: ex });
  }
});

router.delete("/delete/:id", async (req, res, next) => {
  const { id } = req.params;
  await models.Photo.destroy({
    where: {
      id
    }
  });
  res.json({ deleted: id });
});

module.exports = router;

These are all the routes for manipulating photos. The models file is created by Sequelize CLI, which contains the model objects which we manipulate to save our data to the Photos table. We have a GET route to get the photos with findAll, a POST route for saving a Photo with create, a PUT route that updates the model with update, and a DELETE route that deletes a Photo with destroy.

To add file upload, we use the multer package which we installed earlier. It is very simple to add. We just specify the file name and folder to upload the file to, like in this block of code:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./uploads");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});

const upload = multer({
  storage
});

We include the multer middleware with the routes we want to access files with like so:

upload.single("photo")

This specifies that we let the frontend upload a file in the FormData with the photo field.

Then we can access the file path of the saved file in the routes by using req.file.path to save the path to our database.

We need to create an uploads folder in the backend folder to save the files.

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

var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");

var indexRouter = require("./routes/index");
var photosRouter = require("./routes/photos");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(cors());

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "uploads")));

app.use("/", indexRouter);
app.use("/photos", photosRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

In this file, we added app.use(cors()); to enable frontend cross-domain requests to backend. We expose the photos routes to the frontend with the following code:

`var photosRouter = require(“./routes/photos”);
`app.use("/photos", photosRouter);

And we add a static path route to access the files:

app.use(express.static(path.join(__dirname, "uploads")));

Now that back end is done, we can move on to frontend.

Frontend: File Upload with React

We will use React to build the frontend with MobX for simple state management. To create the skeleton code, we run npx create-react-app frontend.

Next we have to install some packages. We will install MobX, Bootstrap for styling, React Router for routing, Formik and Yup for form value handling and form validation respectively, and Axios for making HTTP requests.

To do this run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup to install all the packages.

With all the packages installed, we can start writing code. To start, we replace the existing code in App.js with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import { photosStore } from "./store";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} photosStore={photosStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

This adds a top bar which we will create next, and we define the routes so that we see the home page and address generator page when we go to the defined URLs.

Next create HomePage.js in the src for our home page. In it, we will display the table of entries, which has the name, description, the photo, and buttons to open the add / edit photo forms, and delete the entries.

In the file, we add:

import React, { useState, useEffect } from "react";
import * as yup from "yup";
import "./HomePage.css";
import Modal from "react-bootstrap/Modal";
import PhotoForm from "./PhotoForm";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getPhotos, deletePhoto, APIURL } from "./requests";

function HomePage({ photosStore }) {
  const [show, setShow] = useState(false);
  const [showEdit, setShowEdit] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedPhoto, setSelectedPhoto] = useState({});
  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);

  const handleEditClose = () => setShowEdit(false);
  const handleEditShow = photo => {
    setSelectedPhoto(photo);
    setShowEdit(true);
  };

  const getAllPhotos = async () => {
    const response = await getPhotos();
    photosStore.setPhotos(response.data);
  };

  const deletePhotoById = async id => {
    await deletePhoto(id);
    await getAllPhotos();
  };

  const onSave = () => {
    setShow(false);
    setShowEdit(false);
  };

  useEffect(() => {
    if (!initialized) {
      getAllPhotos();
      setInitialized(true);
    }
  });

  return (
    <div className="home-page">
      <h1>Photos</h1>
      <Button variant="primary" onClick={handleShow}>
        Add Photo
      </Button>

      <Table striped bordered hover style={{ marginTop: 10 }}>
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Photo</th>
            <th></th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {photosStore.photos.map((p, i) => {
            const splitPath = p.photoPath.split("");
            const path = splitPath[splitPath.length - 1];
            return (
              <tr key={i}>
                <td>{p.name}</td>
                <td>{p.description}</td>
                <td>
                  <img src={`${APIURL}/${path}`} style={{ width: 200 }} />
                </td>
                <td>
                  <Button onClick={handleEditShow.bind(this, p)}>Edit</Button>
                </td>
                <td>
                  <Button onClick={deletePhotoById.bind(this, p.id)}>
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>

      <Modal show={show} onHide={handleClose}>
        <Modal.Header closeButton>
          <Modal.Title>Add Photo</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <PhotoForm
            edit={false}
            photosStore={photosStore}
            onSave={onSave.bind(this)}
          />
        </Modal.Body>
      </Modal>

     <Modal show={showEdit} onHide={handleEditClose}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Photo</Modal.Title>
        </Modal.Header>

        <Modal.Body>
          <PhotoForm
            edit={true}
            photosStore={photosStore}
            selectedPhoto={selectedPhoto}
            onSave={onSave.bind(this)}
          />
        </Modal.Body>
      </Modal>
    </div>
  );
}

export default observer(HomePage);

The Table is provided by React Bootstrap. We just display every entry in its own row. The entries are provided by our MobX store which we will create. The modals contain the PhotoForm which has all the inputs for manipulating photos. We will use it for both add and edit so we need to pass in the edit prop to distinguish between add and edit. We also pass in a onSave function so that we can close the modals. If there is an entry selected for edit, we also pass in selectedPhoto prop, which contains the entry that the user is editing.

Next we build the photo form. Create a file called PhotoForm.js in the src folder and add:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import "./PhotoForm.css";
import { getPhotos, addPhoto, editPhoto } from "./requests";
import { observer } from "mobx-react";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  description: yup.string().required("Description is required")
});

function PhotoForm({ photosStore, edit, selectedPhoto, onSave }) {
  const fileUpload = React.createRef();
  const [photo, setPhoto] = useState(null);
  const [fileName, setFileName] = useState("");
  const getAllPhotos = async () => {
    const response = await getPhotos();
    photosStore.setPhotos(response.data);
  };

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      let bodyFormData = new FormData();
      if (!edit) {
        bodyFormData.set("name", evt.name);
        bodyFormData.set("description", evt.description);
        bodyFormData.append("photo", photo);
        await addPhoto(bodyFormData);
      } else {
        bodyFormData.set("id", selectedPhoto.id);
        bodyFormData.set("name", evt.name);
        bodyFormData.set("description", evt.description);
        if (photo) {
          bodyFormData.append("photo", photo);
        }
        await editPhoto(bodyFormData);
      }
    } catch (error) {
      alert("Upload must be an image");
    }

  await getAllPhotos();
    onSave();
  };

  const setFile = evt => {
    setPhoto(evt.target.files[0]);
    setFileName(evt.target.files[0].name);
  };

  const openUploadDialog = () => {
    fileUpload.current.click();
  };

  return (
    <div>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? selectedPhoto : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="description">
                <Form.Label>Description</Form.Label>
                <Form.Control
                  type="text"
                  name="description"
                  placeholder="Description"
                  value={values.description || ""}
                  onChange={handleChange}
                  isInvalid={touched.description && errors.description}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.description}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="photo">
                <input
                  type="file"
                  ref={fileUpload}
                  name="photo"
                  style={{ display: "none" }}
                  onChange={setFile}
                />
                <div className="file-box">
                  <Button type="button" onClick={openUploadDialog}>
                    Upload Photo
                  </Button>
                  <span style={{ paddingLeft: "10px", marginTop: "5px" }}>
                    {fileName}
                  </span>
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Save
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}

export default observer(PhotoForm);

In this file, we have the file input for getting the photo file and the form fields for name and description. To get the file, we pass the setFile function into the onChange prop of the file input, where we get the file object by using ev.target.files[0]. The [0] means that we only want the first file.

We wrapped the React Boostrap form inside the Formik component to automatically handle form values, which will be available in the parameter of the handleSubmit function. In that function, we validate the text inputs with the schema.validation function. Then we add the text data and file to the FormData object. After everything is added, we submit the FormData object to our back end via HTTP.

After that, we call getAllPhotos to get the latest data and set the new data in our MobX store. Then we call onSave function, which is passed in from HomePage so that we can close the dialog box after everything is done.

Next create PhotoForm.css and add:

.file-box {
  display: flex;
}

Then create requests.js in the src folder and add:

const axios = require("axios");
export const APIURL = "http://localhost:3000";

export const getPhotos = () => axios.get(`${APIURL}/photos`);

export const addPhoto = data =>
  axios({
    method: "post",
    url: `${APIURL}/photos/add`,
    data,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });

export const editPhoto = data =>
  axios({
    method: "put",
    url: `${APIURL}/photos/edit`,
    data,
    config: { headers: { "Content-Type": "multipart/form-data" } }
  });

export const deletePhoto = id => axios.delete(`${APIURL}/photos/delete/${id}`);

We need these functions to send the requests to the backend. Note that we have config: { headers: { “Content-Type”: “multipart/form-data” } } in the post so that we send form data instead of the default JSON to the back end. Form data can include files but JSON cannot.

Next we create the MobX Store. Create a file called store.js and add:

import { observable, action, decorate } from "mobx";

class PhotosStore {
  photos = [];

  setPhotos(photos) {
    this.photos = photos;
  }
}

PhotosStore = decorate(PhotosStore, {
  photos: observable,
  setPhotos: action
});

const photosStore = new PhotosStore();

export { photosStore };

We have the function setPhotos to put the photo data in the store, which we used in HomePage and PhotoForm and we instantiated it before exporting so that we only have to do it in one place.

Next we create the top bar by creating a TopBar.js file in the src folder and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";

function TopBar({ location }) {
  const { pathname } = location;

  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Photo App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app.

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Photo App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

This adds the Bootstrap CSS file in the link tag and change the title of the app.

After writing all that code, we can run our app. First start the backend by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

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
JavaScript Answers

How to Listen for Text Highlight in an HTML Element with JavaScript?

Sometimes, we want to listen for text highlight in an HTML element with JavaScript.

In this article, we’ll look at how to listen for text highlight in an HTML element with JavaScript.

Listen for Text Highlight in an HTML Element with JavaScript

To listen for text highlight in an HTML element with JavaScript, we can listen to the selectionchange event.

For instance, if we have the following div:

<div>  
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus aliquam iaculis. Pellentesque interdum elit sapien, quis interdum enim laoreet sed. Mauris varius magna ac dapibus molestie. Sed porttitor sapien eget ipsum aliquet, lacinia venenatis lacus finibus. Phasellus in nibh mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed placerat tristique augue, id lacinia massa iaculis eu. Donec sed vestibulum odio. Fusce sit amet congue odio, eu consequat neque. Sed sed mauris id sem malesuada blandit eu at quam.  
</div>

Then we can write:

document.addEventListener("selectionchange", event => {  
  const selection = document.getSelection ? document.getSelection().toString() : document.selection.createRange().toString();  
  console.log(selection);  
})

to listen to the selectionchange event on document , which means we pick up all text selection changes on the page.

In the event handler, we call getSelection to get the text selection if it exists.

Otherwise, we call document.selection.createRange().toString() to get the text selection.

Conclusion

To listen for text highlight in an HTML element with JavaScript, we can listen to the selectionchange event.