Build a PWA with Ionic, Angular, and the WordPress REST API – Part 4

In this weekend side-hustle series, we’re building a Progressive Web App (PWA) for desktop and mobile that delivers WordPress content using Ionic, Angular, and the WordPress API. In Part 1, we got everything setup and working. In Part 2, we created a richer presentation of the WordPress Posts using Ionic cards. In Part 3, we implemented the single post view. Today, we’ll implement paging functionality using Ionic’s Infinite Scroll (ion-infinite-scroll).

Part 3 Recap

Here’s a quick recap of what we accomplished in Part 3.

  • Polished the presentation
  • Added a Read More… button to post preview cards
  • Added a PostPage component for viewing single posts
  • Updated the DataService to return single post based on post slug given as route parameter
  • Configured the parameterized route to the Post page
  • Made revised source code available on GitHub at https://github.com/codyburleson/ionic-ng-wp-client.

The final result of Part 3 is still pretty simple, but it’s evolving. After Part 3, and starting now, our app looks like this…

Part 4 game plan

The game plan for today is to implement paging functionality using Ionic’s Infinite Scroll feature. Currently, our app only displays a single page of ten posts (the WordPress API default amount). We need a way to load up additional posts when more exist. With Ionic’s Infinite Scroll (ion-infinite-scroll) feature, additional posts will be automatically loaded and displayed when the user scrolls to the bottom of the list.

Update the DataService to get more posts

Update the DataService (src/app/shared/data.service.ts) so that it is as follows…

import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient, HttpResponse} from '@angular/common/http';

import 'rxjs/add/operator/map';
import {of} from 'rxjs/observable/of';
import {Observable} from 'rxjs';

const ENDPOINT_URL = environment.endpointURL;

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

    items: any[] = [];
    page = 1;
    totalPages = 1;

    constructor(private http: HttpClient) {
    }

    /**
     * Gets a page of posts or all posts formerly fetched
     */
    getPosts(): any {
        console.log('> DataService.getPosts');
        if (this.items.length > 0) {
            return of(this.items);
        } else {
            return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed', {observe: 'response'})
                .map(this.processPostData, this);
        }
    }

    /**
     * Gets the next page of posts
     */
    getMorePosts(): any {
        this.page++;
        return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed&page=' + this.page, {observe: 'response'})
            .map(this.processPostData, this);
    }

    // A place for post-processing, before making the fetched data available to view.
    processPostData(resp: HttpResponse<any[]>) {
        this.totalPages = +resp.headers.get('X-WP-TotalPages'); // unary (+) operator casts the string to a number
        resp.body.forEach((item: any) => {
            this.items.push(item);
        });
        return this.items;
    }

    getPostBySlug(slug): any {
        return this.items.find(item => item.slug === slug);
    }

    hasMorePosts() {
        return this.page < this.totalPages;
    }

}

What’s going on here?

  • Line 17 – We added a class variable called page to track the current or last page (set of posts) that we’ve loaded.
  • Line 18 – We added a class variable called totalPages to hold the total number of pages of posts that are available. This is needed so that we can disable the infinite scroll component when we’ve reached the last of the available pages. If we don’t disable the component, it will try to fetch a page number that isn’t available and thus result in an error.
  • Line 31 – In the getPosts() function, we modified the HttpClient’s get call. We added {observe: 'response'} to specify that we want the whole HttpResponse back, rather than just the body. This is necessary because we need to inspect the response to find the X-WP-TotalPages header, which WordPress gives to tell us how many  total pages are available.
  • Line 39 – We added a getMorePosts() function that increments  the current page variable, gets the next page of posts, and then pushes them onto the array of posts that we already have.
  • Line 47 – We modified the processPostData() function to read the X-WP-TotalPages header and set the totalPages class variable.
  • Line 58 – Finally, we added a hasMorePosts() convenience function to return true or false depending on whether more pages are available to be loaded.

Update the HomePage component to use infinite scroll

Update the HomePage component (src/app/home/home.page.ts) so that it’s now as follows…

import {Component, OnInit, ViewChild, ViewEncapsulation} from '@angular/core';
import {DataService} from '../shared/data.service';
import {environment} from '../../environments/environment';
import {InfiniteScroll} from '@ionic/angular';

@Component({
    selector: 'app-home',
    templateUrl: 'home.page.html',
    styleUrls: ['home.page.scss'],
    encapsulation: ViewEncapsulation.ShadowDom
})
export class HomePage implements OnInit {

    @ViewChild(InfiniteScroll) infiniteScroll: InfiniteScroll;

    items: any[];
    dateFormat = environment.dateFormat;

    constructor(public dataService: DataService) {
    }

    ngOnInit() {
        this.dataService.getPosts().subscribe((data: any[]) => {
            this.items = data;
        });
    }

    getMorePosts(evt) {
        this.dataService.getMorePosts().subscribe((data: any[]) => {
            this.items = data;
            this.infiniteScroll.complete();
        });
    }

    infiniteScrollDisabled() {
        if (this.dataService.hasMorePosts()) {
            return false;
        } else {
            return true;
        }
    }

}

What’s going on here?

  • Line 14 – Gives us access to the InfiniteScroll component that we’ll  be using in  the Home view.
  • Line 29 – A new getMorePosts() function pretty much just proxies to the function of the same name that’s in the DataService. I uses this.infiniteScroll.complete() to remove the loading  spinner when it’s done doing its thing.
  • Line 35 – The new infiniteScrollDisabled() function exposes a true/false property that will be bound to the ion-infinite-scroll component in the view. This will be used  to ensure that the infinite scroll component gets disabled when there are no more post pages available to load.

Add ion-infinite-scroll to the HomePage component HTML

We now just need to add the ion-infinite-scroll component to src/app/home/home.page.html. The snippet we need to add is as follows:

<ion-infinite-scroll threshold="100px" (ionInfinite)="getMorePosts($event)" [disabled]="infiniteScrollDisabled()">
  <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
  </ion-infinite-scroll-content>
</ion-infinite-scroll>

The complete contents of src/app/home/home.page.html should now be…

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      Home
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-card *ngFor="let item of items">
    <ion-img *ngIf="item._embedded['wp:featuredmedia']" [src]="item._embedded['wp:featuredmedia'][0].source_url"></ion-img>
    <ion-card-header>
      <ion-card-subtitle>{{ item.date | date:dateFormat }}</ion-card-subtitle>
      <ion-card-title [innerHTML]="item.title.rendered"></ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <div [innerHTML]="item.excerpt.rendered"></div>
      <div>
        <ion-button href="/{{item.slug}}" routerDirection="root" color="primary" size="small">Read more...</ion-button>
      </div>
      Posted in W<br/>
      Tagged X, Y, Z
    </ion-card-content>
  </ion-card>

  <ion-infinite-scroll threshold="100px" (ionInfinite)="getMorePosts($event)" [disabled]="infiniteScrollDisabled()">
    <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>

</ion-content>



Here’s what’s going on. Whenever the user scrolls down within the 100px threshold of the ion-infinite-scrollgetMorePosts($event) is called and the next page is loaded. If no more pages are available, infiniteScrollDisabled() will return true and disable the control so that it doesn’t try to function unnecessarily. It’s pretty freagin’ cool how the ionic control does all this magic with such a simple setup on our part!

And  that’s it! Now, if  you run the app using  ionic serve, you should be able to scroll “infinitely” through all  available posts.

I’m committing this check-point to GitHub with the following message.

Implement paging with ion-infinite-scroll

Wrapping up

Building on Part 3, I showed you how you can implement infinite scroll functionality to page through all available WordPress posts. The app is starting to get useful, but there’s still a lot to do  to make it  cool. I’m not sure what I’ll tackle next, but I will continue to evolve the app, so stay tuned and remember that the evolving source code can always be cloned from GitHub at https://github.com/codyburleson/ionic-ng-wp-client.\

Wanna keep goin? Click the link below for Part 3:

Build a PWA with Ionic, Angular, and the WordPress REST API – Part 5