loading...

BUILDING A BOOK FINDER APP USING GOOGLE BOOK API

fayvik profile image favour vivian woka ・7 min read

We will be building a simple **Book Finder App** that allows users to search and know more about there favorite books. Now let’s write some code!

As you may already know, Angular massively use rxjs behind the scene to handle all sort of asynchronous tasks like Http request, reactive forms, emitting events…etc. You can make the most of the Observables to write less code, and make your workflow easier.

First, create a new Angular project using CLI, make sure you have the latest version of Angular CLI installed

npm install -g angular-cli
ng new BookFinderApp
cd BookFinderApp & npm install
ng serve -o

A local development server will start, you can navigate to in your browser on (http://localhost:4200.)

Creating the search component

Now we are going to create our search component and add to it the simple Html code.

ng g component new

this command will create all the component related files and put them into the newly created view folder, in addition, to register the component in the AppModule so we can use it in the hole application.

Adding our Html

In the view.component.html file, add the HTML code :

<div *ngIf="!loading" class="container-fluid hero-page1">
  <div class="container">
    <div class="row justify-content-center align-items-center">
      <div class="col-lg-12 col-md-12 col-sm-12">
        <h1>Search to know more about your favourite books</h1>
        <input
          [formControl]="queryField"
          id="keyword"
          type="search"
          class="form-control"
          id="exampleInputEmail1"
          aria-describedby="emailHelp"
        />
      </div>
    </div>
  </div>
</div>

<div *ngIf="loading" class="container-fluid mt-4">
  <div class="container">
    <div class="row justify-content-start align-items-start mb-5">
      <div class="col-lg-12">
        <!-- <form [formGroup]="addForm"> formControlName="keyword"  required-->
        <input
          class="form-control"
          [formControl]="queryField"
          type="search"
          id="keyword"
          placeholder="search for artists..."
          typeahead-wait-ms="delay"
          autocomplete="off"
        />
        <!-- </form> -->
      </div>
      <div *ngIf="items" class="col-12 mt-5">
        <div class="row justify-content-around">
          <div
            *ngFor="let product of items"
            class="col-lg-3 col-md-6 col-sm-12 book-display"
          >
            <div class="image">
              <img
                *ngIf="product.volumeInfo.imageLinks.thumbnail"
                src="{{ product.volumeInfo.imageLinks.thumbnail }}"
                alt=""
              />
            </div>
            <div *ngFor="let aut of product.volumeInfo.authors">
              <span> Authors: {{ aut }}</span>
            </div>
            <div class="details">
              <span>Title: {{ product.volumeInfo.title }}</span> <br />
              <br />
              <a [routerLink]="['/new', combineSlug(product.id)]">Details</a>
              <a (click)="goToLink(product.volumeInfo.previewLink)">preview</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

it’s just a simple text input for search and a html tags which will host the search result suggestions and some tags that will display our request.

If you navigate to localhost:4200 you should see the form input which does nothing at the moment.

Bringing some data

For the purpose of this tutorial, I’ll use the Google Book API to have a real database for the search. In order to know more about Google Book API, click on the above link.

Google Book API is intended for developers who want to write applications that can interact with the Books API. Google Books has a mission to digitize the world's book content and make it more discoverable on the Web. The Books API is a way to search and access that content, as well as to create and view personalization around that content.

Using the API

The Google Books API Endpoint is: https://www.googleapis.com/books/v1/volumes?

The API has a lot of parameters that we can use, but we'll only need 3 of them which are :

q: the search query tapped by the user in the search input.

maxResults: The maximum number of results to return. The default is 10.

client_id: the client ID generated in your Google Books Console account.

In order to use the Google Books API, you have to create a developer account and register your app and also to generate an API-KEY.

Creating our search service

ng generate api service

This command will create an api.service.ts file in the app folder, and register this service as a provider in app.module.ts :

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable({
  providedIn: "root"
})
export class ApiService {
  key = "your_key";
  constructor(private httpClient: HttpClient) {}
  get(queryField: string) {
    return this.httpClient.get(
      `https://www.googleapis.com/books/v1/volumes?q=${queryField}&maxResults=39&keyes&key=${this.key}`
    );
  }
}

The service simply makes a GET request to the Google Books API, and return the search result as an observable.

Now that out our service is ready to make an API call, let now connect out search input and the google book API to send value to the database.

Implementing the search feature

Angular has observables behavior already available in a number of places. One of them is inside ReactiveFormsModules, which allows you to use an Observable that is attached to form input. To do that, we have converted our input to use FormControl which expose a valueChange Observable and be before we can use that, we have to import FormModule and ReactiveFormModule:

In the [app.module.ts] let import our FormModule and ReactiveFormModule.

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";
**import { FormsModule, ReactiveFormsModule } from "@angular/forms";**

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { NewComponent } from "./new/new.component";

@NgModule({
  declarations: [AppComponent, NewComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule
    // NgbModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

In view.component.ts we have to import the API service which has our api call and also we have to import FormControl which expose a valueChange Observable.

import { Component, OnInit } from "@angular/core";
import { ApiService } from "../api.service";
**import {
  FormControl,
  FormGroup,
  Validators,
  FormBuilder
} from "@angular/forms";**

@Component({
  selector: "app-view",
  templateUrl: "./view.component.html",
  styleUrls: ["./view.component.scss"]
})
export class ViewComponent implements OnInit {
  items: any;
  loading;
  query: FormControl = new FormControl();
  constructor(
    private apiService: ApiService
  ) {}

  ngOnInit() {
    this.loading = false;
    this.query.valueChanges
    .subscribe( result => console.log(result);
  }
}

in the ngOnInit, we subscribed to the values emitted by the query and logged the results so that you can see the values emitted. Navigate to your browser and check the console to see the value.

If we observe while making our search, we notice that each time the input value changes Angular will fire off a request and handle the response as soon as it is ready. When we space without typing a word it still fires off a request to the server. We don't want it to send an empty value but a word and we don't still want it to fire a request immediately we type. We want it to delay a little before sending a request because we may be typing a long word.

The Solution Approach

Let’s think of an approach, after user types in a query of three characters then we can make an API Hit. In this case, only one meaningful Hit will be made instead of three. We can wait for the user to type the whole query first after that we can make an API call. But how can we do that programmatically? and also how to stop sending an empty value when we tab space without writing a word.

.replace() expression

view.component.ts

.subscribe((query: any) => {
 let te = query.replace(/\s/g, "");
}

In the above code we create a variable and set it to be equal to the value of what we get from our input and we use .replace(/\s/g, "") function to remove space and to stop our input from sending an empty value. But I know you would want to know what expression is that and why it worked. This explanation will help:

It's a regular expression where the \s means "match whitespace" and the g is a flag which means "global", i.e. match all whitespace, not just the first.

Let's remember that it was two problems we encountered and the above code solved just one and we are let with one.

Debounce Time

Debounce Time is the delay that we can add between event subscriptions. Like we can add Debounce Time of 1000 milliseconds which resets after every KeyUp event by a user, if the gap of time between KeyUp event exceeds the 1000 ms then we make a subscription or make API call.

view.component.ts

import {
  debounceTime,
  distinctUntilChanged,
} from "rxjs/operators";

ngOnInit() {
    this.loading = false;
    this.query.valueChanges
    .pipe(debounceTime(10000), distinctUntilChanged())
    .subscribe((query: any) => {
    let te = query.replace(/\s/g, "");
  }
}

If we try making a search now our input we stop sending an empty value and if we type a word it will wait for 1000 ms before it fires a request. I know we would ask why I added this distinctUntilChanged() and what it is.

Since we are reading the text while we type, it is very possible that we will type one character, then type another character and press backspace. From the perspective of the Observable, since it is now debounced by a delay period, it is entirely possible that the user input will be interpreted in such a way that the debounced output will emit two identical values sequentially. RxJS offers excellent protection against this, distinctUntilChanged(), which will discard an emission that will be a duplicate of its immediate predecessor. We added it to avoid that.

Adding our API endpoint to make a real search.

view.component.ts

ngOnInit() {
    this.loading = false;
    this.query.valueChanges
      .pipe(debounceTime(1000), distinctUntilChanged())
      .subscribe((query: any) => {
        let te = query.replace(/\s/g, "");
        if (te.length > 2) {
          this.apiService.get(query).subscribe((result: any) => {
            this.loading = true;
            setTimeout(() => {
              this.items = result.items;
              console.log(this.items);
            }, 3000);
          });
          console.log(query);
        }

        console.log(query);
      });
    console.log(this.query);
  }

Our autosuggest input should now be ready and it should make our request as it passes through the processes we set.

Project

Github
Demo App

Conclusion

Angular and RxJS really change the way we think about single-page applications since it handles events as a stream of data, on which you can make all sorts of data manipulation like debouncing, mapping to values, converting to promise…etc and we also learned how to use GOOGLE BOOK API.

Posted on by:

fayvik profile

favour vivian woka

@fayvik

I'm a Front End Developer based in Port Harcourt, Nigeria. I am passionate about building applications that run on the web. Technical writing is another interest of mine.

Discussion

pic
Editor guide