Exploring the Literary Universe: Building a Library with Infinite Book Feed and Detail Pages using the Gutenberg API (Part 25) in Your Angular-15 Ionic-7 App

 Welcome back to our ongoing series on building a multiplatform application with Angular-15 and Ionic-7! Throughout this journey, we have explored various aspects of app development, including authentication, data management, task organization, calendar integration, event management, media management, UI customization, enhanced user authentication, real-time data manipulation, creating a selectable and searchable data list, building a notepad-style rich text editor, visualizing numerical data with interactive charts, and reading and deleting data with the Supabase PostgreSQL database. Today, in Part 25, we will focus on creating an infinite book library with detail pages using the Gutenberg API.

Books have always been a gateway to knowledge and entertainment. By utilizing the Gutenberg API, we can access a vast collection of books and create a dynamic and immersive book library within our Angular-15 Ionic-7 app.


In this installment, we will guide you through the process of creating an infinite book library with detail pages using the Gutenberg API. We will start by integrating the Gutenberg API into our app and retrieving book data.


Next, we will explore techniques to display the book collection in an infinite feed, allowing users to scroll and explore an extensive library of books. We will also implement detail pages for individual books, enabling users to access additional information, such as author details, book descriptions, and more.

Throughout this tutorial, we will emphasize best practices for working with APIs, data retrieval, pagination, and creating immersive user experiences. By the end of this article, you will have a solid understanding of how to create an infinite book library with detail pages using the Gutenberg API, enhancing the literary exploration capabilities of your Angular-15 Ionic-7 app.

So, join us in Part 25 of our series as we dive into the fascinating world of books and APIs. Together, let's harness the power of the Gutenberg API to create an immersive and dynamic book library within our application.

Here I have created a library page using ionic generate page pages/library where students can access the free gutenberg library(Gutendex) with an infinite scroll. They can view the book details, read the full book online within the browser or they can download the epub format on their device.

For this, I will first add the API URI in the environment.ts file in the following way:

export const environment = {
  production: false,
  //gutenberg api base url
  gutenbergUrl: '<https://gutendex.com/books>',
};

Next, I will create a service by ionic generate service services/library and write the logic for HTTP call in the following way:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
export interface ApiResult {
  page: number;
  results: any;
  total_pages: number;
  total_results: number;
}
@Injectable({
  providedIn: 'root',
})
export class LibraryService {
  constructor(private http: HttpClient) {}

  //import environment variable for gutenbergUrl
  gutenbergUrl = environment.gutenbergUrl;

  getBooks(page = 1): Observable<ApiResult> {
    return this.http.get<ApiResult>(`${this.gutenbergUrl}?page=${page}`);
  }

  getBook(id: any) {
    return this.http.get(`${this.gutenbergUrl}/${id}`);
  }
}

The above code defines an interface ApiResult that describes the structure of the response data returned by the getBooks function in the LibraryService class. The ApiResult interface has four properties:

  • page: a number representing the current page of results returned by the API
  • results: an array of any type representing the books returned by the API
  • total_pages: a number representing the total number of pages of results returned by the API
  • total_results: a number representing the total number of results returned by the API

The LibraryService class has two methods:

  • getBooks: this method makes an HTTP GET request to the Gutenberg API using the HttpClient service and returns an observable of type ApiResult. The page parameter is optional and defaults to 1 if not provided. The URL of the API endpoint is constructed using the gutenbergUrl property defined in the class.
  • getBook: this method makes an HTTP GET request to the Gutenberg API using the HttpClient service and returns an observable of any type (since we don't know the exact structure of the response data for an individual book). The id parameter specifies the ID of the book to retrieve and is used to construct the URL of the API endpoint using the gutenbergUrl property defined in the class.

Next, I will import this service in library.page.ts and write the logic for displaying the books:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  InfiniteScrollCustomEvent,
  IonicModule,
  LoadingController,
} from '@ionic/angular';
import { LibraryService } from 'src/app/services/library.service';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-library',
  templateUrl: './library.page.html',
  styleUrls: ['./library.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, RouterLink],
})
export class LibraryPage implements OnInit {
  books: any[] = [];
  currentPage = 1;

  constructor(
    private libraryService: LibraryService,
    private loadingCtrl: LoadingController
  ) {}

  ngOnInit() {
    this.loadBooks();
  }

  async loadBooks(event?: any) {
    const loading = await this.loadingCtrl.create({
      message: 'Loading...',
      spinner: 'circular',
    });

    await loading.present();

    this.libraryService.getBooks(this.currentPage).subscribe((res) => {
      loading.dismiss();
      this.books.push(...res.results);
      console.log(res.results);

      //below code is to disable the infinite scroll when all the pages are loaded
      event?.target.complete(); // optional chaining operator (?.) to guard against a null value in the event parameter
      if (event) {
        event.target.disabled = res.total_pages === this.currentPage; // if the total_pages is equal to the current page, then disable the infinite scroll
      }
    });
  }

  // infinite scroll to load more data
  loadData(event: InfiniteScrollCustomEvent) {
    // InfiniteScrollCustomEvent is a custom event type that is emitted by the infinite scroll component when it is triggered
    this.currentPage++; // increment the current page
    this.loadBooks(event); // call the loadBooks() method
  }
}

The above code defines a component called LibraryPage, which implements the OnInit interface and contains a list of books and some methods to load more books from an API.

The books array is initialized as an empty array, and currentPage is set to 1.

The ngOnInit method is called when the component is initialized and calls the loadBooks method.

The loadBooks method displays a loading spinner and calls the getBooks method of the LibraryService. The getBooks method returns an observable, which is subscribed to in the loadBooks method. When the observable emits a response, the loading spinner is dismissed, and the response results are pushed to the books array. If an optional event parameter is passed to the loadBooks method (for example, when using infinite scrolling), the event.target.disabled property is set to true if the current page is equal to the total number of pages.

The loadData method is called when the infinite scroll event is triggered and increments the currentPage property before calling the loadBooks method to load more books.

Next, I will write the template for this in library.page.html file like this:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>Library</ion-title>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>

    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Library</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-list>
    <ion-item
      [routerLink]="['/tabs/library', book.id]"
      detail="true"
      lines="full"
      button
      *ngFor="let book of books"
    >
      <ion-col size="4">
        <ion-img
          class="ion-padding"
          [src]='book.formats["image/jpeg"]'
        ></ion-img>
      </ion-col>
      <ion-col size="8">
        <h5><strong> {{book.title}}</strong></h5>
        <p>{{book.authors[0].name}}</p>

        <ion-label>
          <ion-icon slot="start" name="download-outline"></ion-icon>
          {{book.download_count}}</ion-label
        >
      </ion-col>
    </ion-item>
  </ion-list>

  <ion-infinite-scroll (ionInfinite)="loadData($any($event))">
    <ion-infinite-scroll-content
      loadingSpinner="crescent"
      loadingText="Loading more data..."
    >
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>
</ion-content>

This is an Ionic framework HTML template for a Library page. It includes an <ion-header> component with a large title, an <ion-list> component that displays a list of books, and an <ion-infinite-scroll> component that enables the user to load more books as they scroll down.

The list of books is created using an *ngFor directive that iterates through an array of books (books) and creates an <ion-item> component for each book. Each <ion-item> component displays a thumbnail image, book title, author name, and download count. When the user clicks on a book item, it navigates them to a detail page for that book.

The <ion-infinite-scroll> component is used to enable the user to load more books as they scroll down. When the user reaches the bottom of the page, the (ionInfinite) event is triggered, which calls the loadData() method to load the next set of books.

Now I will create a details page that will show the details of each book using Angular routing with the command ionic generate page pages/library-details and apply the id paramter in the tabs.route.ts file like this:

{
        path: 'library/:id',
        loadComponent: () =>
          import('../library-details/library-details.page').then(
            (m) => m.LibraryDetailsPage
          ),
      },

Now I will write the logic within the library-details.page.ts file in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ActivatedRoute } from '@angular/router';
import { LibraryService, ApiResult } from 'src/app/services/library.service';

@Component({
  selector: 'app-library-details',
  templateUrl: './library-details.page.html',
  styleUrls: ['./library-details.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule],
})
export class LibraryDetailsPage implements OnInit {
  book: any = {
    // initialize the book object with empty values for all the properties that are used in the template
    title: '',
    authors: [
      {
        name: '',
        birth_year: '',
        death_year: '',
      },
    ],
    subjects: [],
    download_count: 0,
    formats: {},
    bookshelves: [],
    languages: [],
    copyright: false,
    media_type: '',
  };

  constructor(
    private activatedRoute: ActivatedRoute,
    private libraryService: LibraryService
  ) {}

  ngOnInit() {
    const id: any = this.activatedRoute.snapshot.paramMap.get('id');
    // console.log(id);
    this.libraryService.getBook(id).subscribe((res) => {
      this.book = res;
      console.log(res);
    });
  }

  downloadEPUBBook() {
    const url = this.book.formats['application/epub+zip'];
    // console.log(url);
    window.open(url);
  }

  readOnline() {
    const url = this.book.formats['text/html'];
    // console.log(url);
    window.open(url);
  }
}

The above component is implementing the OnInit interface which means that it has a ngOnInit() method that will be called after Angular has initialized all data-bound properties of a directive.

The component has a book object with empty values for all the properties that are used in the template. This object is used to display book details in the template.

The constructor of this component takes two parameters: activatedRoute and libraryService. The activatedRoute parameter is used to get the route parameters and the libraryService parameter is used to fetch book details from an external library service.

The ngOnInit() method gets the id parameter from the route using activatedRoute.snapshot.paramMap.get('id') and calls the getBook() method of the libraryService to fetch the book details. The fetched details are then assigned to the book object.

The component also has two methods, downloadEPUBBook() and readOnline(). These methods are used to open the EPUB book and read it online respectively. The methods get the URL of the book from the book object and open it in a new window using the window.open() method.

And finally, I will write the template for this details page in library-details.page.html in the following way:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>{{book.title}}</ion-title>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/tabs/library"></ion-back-button>
    </ion-buttons>
    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">{{book.title}}</ion-title>
    </ion-toolbar>
  </ion-header>
  <ion-card>
    <ion-img [src]='book.formats["image/jpeg"]'></ion-img>
    <ion-card-header>
      <ion-card-subtitle>
        {{book.authors[0].name}} | {{book.authors[0].birth_year}} -
        {{book.authors[0].death_year}}
      </ion-card-subtitle>

      <ion-card-title> {{book.title}} </ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <ion-list lines="none">
        <ion-item>Subjects: </ion-item>
        <ion-chip
          *ngFor="let subject of book.subjects"
          color="tertiary"
          outline="false"
        >
          <ion-label>{{subject}}</ion-label>
        </ion-chip>

        <ion-item *ngIf="book.bookshelves.length">Bookshelves: </ion-item>
        <ion-chip
          outline="false"
          color="secondary"
          *ngFor="let bookshelf of book.bookshelves"
        >
          <ion-label>{{bookshelf}}</ion-label>
        </ion-chip>

        <ion-item>
          <ion-label>Languages: </ion-label>
          <ion-chip
            *ngFor="let language of book.languages"
            color="primary"
            outline="false"
          >
            <ion-label>{{language}}</ion-label>
          </ion-chip>
        </ion-item>

        <ion-item>
          <ion-label>Copyright: {{book.copyright ? 'Yes' : 'None'}} </ion-label>
        </ion-item>

        <ion-item>
          <ion-label> Downloads: {{book.download_count}} </ion-label>
        </ion-item>

        <ion-button
          class="ion-margin"
          shape="round"
          fill="solid"
          expand="full"
          (click)="readOnline()"
        >
          <ion-icon slot="start" name="book-outline"></ion-icon>
          Read Online
        </ion-button>
        <ion-button
          shape="round"
          class="ion-margin"
          fill="outline"
          expand="full"
          (click)="downloadEPUBBook()"
        >
          <ion-icon slot="start" name="download-outline"></ion-icon>
          Download EPUB
        </ion-button>
      </ion-list>
    </ion-card-content>
  </ion-card>
</ion-content>

In the above code, the page header is defined using the ion-header and ion-toolbar elements. The ion-title displays the book title, which is dynamically bound to the book.title property.

The main content of the page is defined within an ion-card element. It contains an image of the book cover, displayed using the ion-img element with the src attribute set to book.formats["image/jpeg"].

The book details are displayed using the ion-card-header and ion-card-content elements. The book's author is displayed in the subtitle, and the title is displayed in the title section. The book details are displayed in an ion-list element with various ion-item elements for the different book details, such as subjects, bookshelves, languages, copyright, and downloads.

The page also has two buttons: one to read the book online, and one to download the EPUB format of the book. The buttons are defined using the ion-button element and include icons and text. Clicking on the buttons calls the readOnline() or downloadEPUBBook() methods, which are defined in the component class.

The library feature is demonstrated in figures 97, 98, and 99 below:

Figure 97: Library Infinite Scroll


Figure 98: Book Detail


Figure 99: Book Download

Conclusion

In this twenty-fifth installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the creation of an infinite book library with detailed pages using the Gutenberg API. By leveraging this powerful API, we were able to access a vast collection of books and create an immersive reading experience within our Angular-15 Ionic-7 app.

Books have always held a special place in our lives, and with the Gutenberg API, we brought the magic of literature to our application. Throughout this tutorial, we guided you through the process of integrating the Gutenberg API, retrieving book data, and implementing an infinite feed of books for users to explore.

We also implemented detail pages for individual books, enriching the user experience by providing additional information and context about each book. By following best practices for working with APIs, data retrieval, and creating immersive user experiences, you now have the tools and knowledge to create your own infinite book library within your Angular-15 Ionic-7 app.

We hope this tutorial has inspired you to dive into the world of literature and APIs, creating a captivating reading experience for your users. By utilizing the Gutenberg API, you can offer a diverse range of books and create an app that caters to the interests and preferences of book lovers.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, and the Gutenberg API. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more dynamic, engaging, and user-friendly.

Happy coding and happy reading!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript