Rapid Prototyping: Ionic 4 and Wordpress

Check out our new guide on web scraping!:

The final code for this guide can be found here.

Goal

In this guide, we’re going to prototype a mobile application using Ionic 4. We’ll be showcasing a news app that reads content from a Wordpress website.

To keep things simple, this application will mostly display the latest posts and categories (fetching the WP site live). If you are interested in a guide on any of these more advanced extensions, please drop me a comment:

  • Integrating redux with the Ngrx library.
  • Adding a login with an authorization wall (preventing users who are not logged in from accessing certain content)
  • Adding paid subscription plans with Stripe
Background

Ionic

Ionic is a framework for building cross-platform applications with a single codebase. It builds to iOS, Android, and web, using the familiar and robust HTML/ CSS/ JavaScript development environment. Ionic can be pretty opinionated about styles and theming; it ships with a plethora of components that require no extension, but maintain a consistent design. Since these components 1.) Include very efficient mobile / web UI widgets and 2.) Abide by best design practices this is ideal. The downside is that beyond color and size, controlling the presentation will require some extra work. So if you’re trying to produce an MVP, Ionic is perfect. If you’re trying to match your designers Photoshop / Illustrator renderings, it may be more difficult.

The Ionic Platform takes the Web Application you create, and ports it into ‘webviews’, containers that are configured to run on different platforms (i.e. handle the differences in operating systems for you). In short, the application that you write runs on another layer. Contrast that with various other cross-platform frameworks, that actually compile user code into native applications. These two approaches have their own pros and cons. The main tradeoffs you should keep in mind when deciding if Ionic is right for you:

Advantages of Ionic Approach

  • (Arguably) faster development
  • Builds a website in addition to native mobile apps
  • More consistent interface across different platforms
  • Comes with a robust toolset
    • The Ionic Dev App allows you to see your changes as you make them, directly to a mobile device.
    • You can also see live updates to the browser application, using the 'ionic serve' command
Disadvantages of Ionic Approach
  • Performance can be hindered due to the extra layer. This means you have to be careful with your UI, that your app renders efficiently. For example, using Infinite Scroll will prevent slowing down the device.
  • Native mobile device functionality does not entirely come out of the box; you will have to install a Cordova Plugin to use the device's camera and the like.
  • The plugin ecosystem is fairly diverse, so for all standard device integrations this involves a simple command
In short, if your concern is time, the Webview is a great angle. That's not to say you can't build a very scalable Ionic Application, either. At K-Optional, we immediately consider Ionic for clients who are building mobile applications.

Ionic 4 (beta)

Ionic has always been nicely integrated with Angular (and previously Angular.js). With the newly released version 4, it seeks to be framework-agnostic (is not coupled to a specific framework).

The development team has announced they will soon release integrations with Vue.js and React, meaning you will be able to leverage the Ionic platform with anyone of the big three frameworks. Version 4 already has support for Angular, the framework this tutorial will use.

Getting Started

Creating an Ionic 4 Application

Note: we will try to be succinct with this guide, but will link out to the documentation / guides of frameworks used when appropriate.

Full Ionic “Getting Started” guide

Let’s create a new Ionic Project. I assume you have npm installed on your command-line; if not, head on over to the link above, where there is more information.

Go ahead and install Ionic. Make sure it’s Ionic 4 (run the update command below if not)

# if you already have ionic installed
npm update -g ionic
# If you have not installed ionic
npm install -g ionic

#NOTE if your global npm directory is user protected, you may need to use sudo in front of the command:
sudo npm install -g ionic

Then use the Ionic command-line tool to generate a new application and go into that directory.

ionic start ionicwordpress sidemenu --type=angular
# Fill out the prompts how you like
# For the purpose of this demo, it's not necessary to integrate the app with native iOS and Android, but I usually do
# And don't worry about ionic pro

#cd into the new directory
cd ionicwordpress

# If you didn't select to install dependencies in the prompts:
npm install

Next start the Ionic server. This will open a browser window with your application. It will restart each time you update and save your code.

ionic serve --lab

Finally, open up the project with your code editor of choice.

Getting setup with Wordpress

We need a Wordpress website to fetch data from for our app. For this guide, we will use the url: https://boxingtldr.com. If you’d rather use your own Wordpress site, just swap out references for that URL with your own. Setting up a self-hosted Wordpress site is outside the scope of this guide, but there are plenty of great resources online.

Step 1: Getting to know the Wordpress REST API

Wordpress REST Handbook

Wordpress websites expose an API by default. In our case, this means that you can access content from a Wordpress site using an Ajax request. In short, you may load data without refreshing the page in a standardized and predictable manner.

Here’s what I mean, head on over to the posts at: https://boxingtldr.com/wp-json/wp/v2/posts. This is a machine readable way to fetch the most recent posts from the website. Our application will request the data and render it into our app. We will fetch things like posts, categories and tags. All of these have their own endpoint (see REST guide above).

Step 2: Setting up the app’s pages

Eventually, this application is going to display and organize content from the WP site. It’s more efficient to focus on the views first, and then worry about shoehorning content into the appropriate places.

We are going to create three pages in total:

  1. A PostList Page (actually, the home page) for displaying the most recent posts
  2. A CategoryPage for displaying a list of categories to filter posts by
  3. A PostDetail Page for viewing all the content of a single page

Deleting the unused list page

When the Ionic command-line tool creates an application, it typically will start with a home page and list page. We’ll reuse the home page, and delete the list page.

Remove the folder in src/app called “list”. If your Ionic Server is running, it will crash at this moment. That’s because it imports the ListPage which no longer exists. We’ll fix this when we add the new pages.

Generating the other pages

As aforementioned, this application will consist of three different pages. The home page should already exist. If it does not, generate it in the same manner as we generate the categories page and post detail page below.

# Create the post detail page
ionic generate page PostDetail

# create the categories page
ionic generate page Categories

You will see some output indicating the pages have been generated in the src/app/post-detail & src/app/categories directories. Now update app-routing.module with the following:

import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";

const routes: Routes = [
  {
    path: "",
    redirectTo: "home",
    pathMatch: "full",
  },
  {
    path: "home",
    loadChildren: "./home/home.module#HomePageModule",
  },
  {
    path: "home/:category",
    loadChildren: "./home/home.module#HomePageModule",
  },
  {
    path: "posts/:id",
    loadChildren: "./post-detail/post-detail.module#PostDetailPageModule",
  },
  {
    path: "categories",
    loadChildren: "./categories/categories.module#CategoriesPageModule",
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

This code defines the URL routes with the corresponding page. It should be pretty straightforward. You’ll notice that the home page is assigned two routes. When selecting a category, we want the home page to appear with only posts of that category. This will make more sense later on.

Step 3: Hooking up to the website’s API

To retrieve data from the website’s WP API, we’ll use an Angular Service. The Ionic CLI can generate services which any page may import. The app must be capable of fetching:

  • (recent) Posts
  • Categories
  • Posts by category
  • A specific post
You might consider creating multiple services in a more complex application. However, one service is plenty for four API methods. In this step, we'll generate that service. It uses another Angular service plus an npm module, so we'll add these first.

Add dependencies to app.module.ts

HttpClient is a service that ships with Angular for making HTTP requests. Using it requires one small update to our app.module.ts.

import { HttpClientModule } from "@angular/common/http";
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouteReuseStrategy } from "@angular/router";
import { SplashScreen } from "@ionic-native/splash-screen/ngx";
import { StatusBar } from "@ionic-native/status-bar/ngx";
import { IonicModule, IonicRouteStrategy } from "@ionic/angular";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    HttpClientModule,
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

In line 1, we imported the module that contains HttpClient. Then in line 18, we added it to the imports array so that our service can use it. If you want more information on what the module file does, check out Angular’s docs.

Add Lodash to the project

Lodash is a wonderful library that provides all sorts of utilities, useful in countless situations. I include it in nearly every project. Simply install it using npm:

npm install lodash --save

Now we’re ready to write the service.

Generate the posts service

You can generate the service using the command:

ionic generate service Posts

Now add the following to src/app/posts-service.ts:

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

import { map } from "rxjs/operators";
import get from "lodash/get";

@Injectable({
  providedIn: "root"
})
export class PostsService {
  constructor(private http: HttpClient) {}
  private baseURL = "https://boxingtldr.com";
  fetchPosts() {
    return this.http
      .get(`${this.baseURL}/wp-json/wp/v2/posts?_embed`)
      .pipe(
        map((posts: Array<any>) => posts.map(this.setEmbeddedFeaturedImage))
      );
  }

  fetchPost(post_id: string) {
    return this.http
      .get(`${this.baseURL}/wp-json/wp/v2/posts/${post_id}?_embed`)
      .pipe(map((post: any) => this.setEmbeddedFeaturedImage(post)));
  }

  /**
   * Makes the featured image parameter easily accessible in a template
   */
  private setEmbeddedFeaturedImage(p) {
    return Object.assign({}, p, {
      featured_image: get(p, "_embedded['wp:featuredmedia'][0].source_url")
    });
  }

  fetchPostCategories() {
    return this.http.get(`${this.baseURL}/wp-json/wp/v2/categories`);
  }

  fetchPostsByCategory(category_id: string) {
    return this.http
      .get(
        `${this.baseURL}/wp-json/wp/v2/posts?_embed&categories=${category_id}`
      )
      .pipe(
        map((posts: Array<any>) => posts.map(this.setEmbeddedFeaturedImage))
      );
  }
}

Admittedly, this is a lot. This service:

  • Provides methods for making HTTP requests to the relevant WP endpoints
  • uses the map operator to process the HTTP response on the fly
  • Attaches the featured image url to the post if there is one.
    • The "_embed" query parameter signals to the API to include certain useful properties on the post.
    • The "setEmbeddedFeaturedImage" method adds the featured image to the top level  of the post. Otherwise it's nested way down, and makes our templates complicated. It uses Lodash's "get" method, which will not throw an error if one of the parent properties is undefined.
If you want to use a different Wordpress site, replace the 'baseURL' property with your own URL.

Step 4: Displaying the data

Our app now retrieves Wordpress data (see Step 3). It also contains pages where the data will be displayed (see Step 2). We just need to configure these pages to display the data. Let’s start with the PostDetail page:

PostDetail Page

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, ParamMap } from "@angular/router";
import { Observable } from "rxjs";
import { switchMap } from "rxjs/operators";
import { PostsService } from "./../posts.service";

@Component({
  selector: "app-post-detail",
  templateUrl: "./post-detail.page.html",
  styleUrls: ["./post-detail.page.scss"]
})
export class PostDetailPage implements OnInit {
  constructor(private route: ActivatedRoute, private postSrvc: PostsService) {}
  post$: Observable<any>;
  ngOnInit() {
    this.post$ = this.route.paramMap.pipe(
      switchMap((params: ParamMap) => this.postSrvc.fetchPost(params.get("id")))
    );
  }
}
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title> Post Detail </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <div *ngIf="post$ | async as post">
    <h1 [innerHTML]="post.title.rendered"></h1>
    <p no-margin><small> By {{post._embedded.author[0].name }} </small></p>
    <div text-center padding *ngIf="post.featured_image">
      <img
        class="featured_image"
        [src]="post.featured_image"
        alt="Featured Image"
      />
    </div>
    <div [innerHTML]="post.content.rendered"></div>
  </div>
</ion-content>
.featured_image {
  max-width: 100%;
  max-height: 200px;
}
  • The post$ property contains an Observable (it is convention to name observables with a trailing dollar sign)
    • It uses the switchMap operator to pipe the route id into a service request for that post
    • The built in Async Pipe allows the template to reference each change to the post$ property as it's own variable. This powerful syntax saves a lot of code.
  • The innerHTML attribute (see docs) renders HTML directly into the template.
    • The WP REST API returns markup directly so this is the only way to display it.
    • This technique should not be used on content that you don't have control over.
Next, let's add the home page (the PostList Page)

Home Page

import { PostsService } from "./../posts.service";
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { Router } from "@angular/router";
import { ActivatedRoute, ParamMap } from "@angular/router";
import { switchMap } from "rxjs/operators";

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"]
})
export class HomePage implements OnInit {
  constructor(
    private postSrvc: PostsService,
    private router: Router,
    private route: ActivatedRoute
  ) {}

  posts$: Observable<any>;
  loadPost(post: any) {
    this.router.navigate(["/posts", post.id]);
  }
  ngOnInit() {
    this.posts$ = this.route.paramMap.pipe(
      switchMap(
        (params: ParamMap) =>
          params.get("category")
            ? this.postSrvc.fetchPostsByCategory(params.get("category"))
            : this.postSrvc.fetchPosts()
      )
    );
  }
}
<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>
  <div
    (click)="loadPost(post)"
    padding-bottom
    *ngFor="let post of posts$ | async"
  >
    <div>
      <h3 [innerHTML]="post.title.rendered"></h3>
      <p no-margin><small> By {{post._embedded.author[0].name }} </small></p>
    </div>
    <div *ngIf="post.featured_image">
      <img padding-top class="featured-image" [src]="post.featured_image" />
    </div>
    <div>
      <div [innerHTML]="post.excerpt.rendered"></div>
    </div>
  </div>
</ion-content>
.readmore {
  display: none;
}

.featured-image {
  max-height: 150px;
  margin-left: auto;
  margin-right: auto;
}

This page should appear similar to the post-detail page. The main differences are:

  • It uses the ngFor directive to loop through all posts.
  • The excerpt is rendered instead of the content.
  • Upon initialization, the page checks to see if there is a category ID in the route. It conditionally determines which service method to call based on that.
  • When a user clicks a post, we use the Angular Router to programmatically navigate to the /posts/:id route.

The Categories Page

import { Observable } from "rxjs";
import { PostsService } from "./../posts.service";
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-list",
  templateUrl: "categories.page.html",
  styleUrls: ["categories.page.scss"]
})
export class CategoriesPage implements OnInit {
  public categories$: Observable<any>;
  public items: Array<{ title: string; note: string; icon: string }> = [];
  constructor(private postSrvc: PostsService) {}

  ngOnInit() {
    this.categories$ = this.postSrvc.fetchPostCategories();
  }
}
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title> List </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item
      [routerLink]="['/home', item.id]"
      *ngFor="let item of categories$ | async"
    >
      <ion-icon name="search" slot="start"></ion-icon>
      {{item.name}}
      <div class="item-note" slot="end">{{item.note}}</div>
    </ion-item>
  </ion-list>
  <!--
    <div *ngIf="selectedItem" padding>
      You navigated here from <b>{{selectedItem.title }}</b>
    </div>
  -->
</ion-content>

(categories.page.scss or categories.page.css should be empty)

Last but not least, the categories page proves to be the simplest. One thing to note, we used the routerLink attribute in the template unlike the home page. Clicking a category loads the home page with a category id as a parameter.

Conclusion

We just created a news mobile application!   Obviously, it leaves a lot to be desired on the user-interface. But Ionic makes that part easy. You can check out the Ionic Component Documentation for ways you can spice up the look and feel.

Hopefully we’ve demonstrated a way to combine two time-tested and powerful frameworks. If anything didn’t make sense feel free to email developer@koptional.com or drop a comment. As a reminder, all the code can be found here.