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 APIresults
: an array of any type representing the books returned by the APItotal_pages
: a number representing the total number of pages of results returned by the APItotal_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 theHttpClient
service and returns an observable of typeApiResult
. Thepage
parameter is optional and defaults to 1 if not provided. The URL of the API endpoint is constructed using thegutenbergUrl
property defined in the class.getBook
: this method makes an HTTP GET request to the Gutenberg API using theHttpClient
service and returns an observable of any type (since we don't know the exact structure of the response data for an individual book). Theid
parameter specifies the ID of the book to retrieve and is used to construct the URL of the API endpoint using thegutenbergUrl
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!