How to create an Ionic 4 PWA with capacitor?

How to Build Mobile Apps with Angular, Ionic 4, and Spring Boot

How to Build Mobile Apps with Angular, Ionic 4, and Spring Boot

Run Your Ionic App on Android. Make sure you're using Java 8. Run ionic cordova prepare android. Open platforms/android in Android Studio, upgrade Gradle if prompted. Set launchMode to singleTask in AndroidManifest.xml. Start your app using Android Studio...

In this brief tutorial, I’ll show you to use Ionic for JHipster v4 with Spring Boot and JHipster 6.

To complete this tutorial, you’ll need to have Java 8+, Node.js 10+, and Docker installed. You’ll also need to create an Okta developer account.

Create a Spring Boot + Angular App with JHipster

You can install JHipster via Homebrew (brew install jhipster) or with npm.

npm i -g [email protected]

Once you have JHipster installed, you have two choices. There’s the quick way to generate an app (which I recommend), and there’s the tedious way of picking all your options. I don’t care which one you use, but you must select OAuth 2.0 / OIDCauthentication to complete this tutorial successfully.

Here’s the easy way:

mkdir app && cd app

echo "application { config { baseName oauth2, authenticationType oauth2, \
  buildTool gradle, testFrameworks [protractor] }}" >> app.jh

jhipster import-jdl app.jh

The hard way is you run jhipster and answer a number of questions. There are so many choices when you run this option that you might question your sanity. At last count, I remember reading that JHipster allows 26K+ combinations!

The project generation process will take a couple of minutes to complete if you’re on fast internet and have a bad-ass laptop. When it’s finished, you should see output like the following.

OIDC with Keycloak and Spring Security

JHipster has several authentication options: JWT, OAuth 2.0 / OIDC, and UAA. With JWT (the default), you store the access token on the client (in local storage). This works but isn’t the most secure. UAA involves using your own OAuth 2.0 authorization server (powered by Spring Security), and OAuth 2.0 / OIDC allows you to use Keycloak or Okta.

Spring Security makes Keycloak and Okta integration so incredibly easy it’s silly. Keycloak and Okta are called "identity providers" and if you have a similar solution that is OIDC-compliant, I’m confident it’ll work with Spring Security and JHipster.

Having Keycloak set by default is nice because you can use it without having an internet connection.

To log into the JHipster app you just created, you’ll need to have Keycloak up and running. When you create a JHipster project with OIDC for authentication, it creates a Docker container definition that has the default users and roles. Start Keycloak using the following command.

docker-compose -f src/main/docker/keycloak.yml up -d

Start your application with ./gradlew (or ./mvnw if you chose Maven) and you should be able to log in using "admin/admin" for your credentials.

Open another terminal and prove all the end-to-end tests pass:

npm run e2e

If your environment is setup correctly, you’ll see output like the following:

> [email protected] e2e /Users/mraible/app
> protractor src/test/javascript/protractor.conf.js

[16:02:18] W/configParser - pattern ./e2e/entities/**/*.spec.ts did not match any files.
[16:02:18] I/launcher - Running 1 instances of WebDriver
[16:02:18] I/direct - Using ChromeDriver directly...


  account
    ✓ should fail to login with bad password
    ✓ should login successfully with admin account (1754ms)

  administration
    ✓ should load metrics
    ✓ should load health
    ✓ should load configuration
    ✓ should load audits
    ✓ should load logs


  7 passing (15s)

[16:02:36] I/launcher - 0 instance(s) of WebDriver still running
[16:02:36] I/launcher - chrome #01 passed
Execution time: 19 s.

OIDC with Okta and Spring Security

To switch to Okta, you’ll first need to create an OIDC app. If you don’t have an Okta Developer account, now is the time!

Log in to your Okta Developer account.

  • In the top menu, click on Applications
  • Click on Add Application
  • Select Web and click Next
  • Enter JHipster FTW! for the Name (this value doesn’t matter, so feel free to change it)
  • Change the Login redirect URI to be <a href="http://localhost:8080/login/oauth2/code/oidc" target="_blank">http://localhost:8080/login/oauth2/code/oidc</a>
  • Click Done, then Edit and add <a href="http://localhost:8080" target="_blank">http://localhost:8080</a> as a Logout redirect URI
  • Click Save

These are the steps you’ll need to complete for JHipster. Start your JHipster app using a command like the following:

SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://{yourOktaDomain}/oauth2/default \
  SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=$clientId \
  SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=$clientSecret ./gradlew

Create a Native App for Ionic

You’ll also need to create a Native app for Ionic. The reason for this is because Ionic for JHipster is configured to use PKCE(Proof Key for Code Exchange). The current Spring Security OIDC support in JHipster still requires a client secret. PKCE does not.

Go back to the Okta developer console and follow the steps below:

  • In the top menu, click on Applications
  • Click on Add Application
  • Select Native and click Next
  • Enter Ionic FTW! for the Name
  • Add Login redirect URIs: <a href="http://localhost:8100/implicit/callback" target="_blank">http://localhost:8100/implicit/callback</a> and dev.localhost.ionic:/callback
  • Click Done, then Edit and add Logout redirect URIs: <a href="http://localhost:8100/implicit/logout" target="_blank">http://localhost:8100/implicit/logout</a> and dev.localhost.ionic:/logout
  • Click Save

You’ll need the client ID from your Native app, so keep your browser tab open or copy/paste it somewhere.

Create Groups and Add Them as Claims to the ID Token

In order to login to your JHipster app, you’ll need to adjust your Okta authorization server to include a groups claim.

On Okta, navigate to Users > Groups. Create ROLE_ADMIN and ROLE_USER groups and add your account to them.

Navigate to API > Authorization Servers, click the Authorization Servers tab and edit the default one. Click the Claims tab and Add Claim. Name it "groups" or "roles" and include it in the ID Token. Set the value type to "Groups" and set the filter to be a Regex of .*. Click Create.

Navigate to <a href="http://localhost:8080" target="_blank">http://localhost:8080</a>, click sign in and you’ll be redirected to Okta to log in.

Enter the credentials you used to signup for your account, and you should be redirected back to your JHipster app.

Generate Entities for a Photo Gallery

Let’s enhance this example a bit and create a photo gallery that you can upload pictures to. Kinda like Flickr, but waaayyyy more primitive.

JHipster has a JDL (JHipster Domain Language) feature that allows you to model the data in your app, and generate entities from it. You can use its JDL Studio feature to do this online and save it locally once you’ve finished.

I created a data model for this app that has an Album, Photo, and Tag entities and set up relationships between them. Below is a screenshot of what it looks like in JDL Studio.

Copy the JDL below and save it in a photos.jdl file in the root directory of your project.

entity Album {
  title String required,
  description TextBlob,
  created Instant
}

entity Photo {
  title String required,
  description TextBlob,
  image ImageBlob required,
  taken Instant
}

entity Tag {
  name String required minlength(2)
}

relationship ManyToOne {
  Album{user(login)} to User,
  Photo{album(title)} to Album
}

relationship ManyToMany {
  Photo{tag(name)} to Tag{photo}
}

paginate Album with pagination
paginate Photo, Tag with infinite-scroll

You can generate entities and CRUD code (Java for Spring Boot; TypeScript and HTML for Angular) using the following command:

jhipster import-jdl photos.jdl

When prompted, type a to update existing files.

This process will create Liquibase changelog files (to create your database tables), entities, repositories, Spring MVC controllers, and all the Angular code that’s necessary to create, read, update, and delete your data objects. It’ll even generate Jest unit tests and Protractor end-to-end tests!

When the process completes, restart your app, and confirm that all your entities exist (and work) under the Entities menu.

You might notice that the entity list screen is pre-loaded with data. This is done by faker.js. To turn it off, edit src/main/resources/config/application-dev.yml, search for liquibase and set its contexts value to dev. I made this change in this example’s code and ran ./gradlew clean to clear the database.

liquibase:
  # Add 'faker' if you want the sample data to be loaded automatically
  contexts: dev

Develop a Mobile App with Ionic and Angular

Getting started with Ionic for JHipster is similar to JHipster. You simply have to install the Ionic CLI, Yeoman, the module itself, and run a command to create the app.

npm i -g [email protected] [email protected] yo
yo jhipster-ionic

If you have your app application at ~/app, you should run this command from your home directory (~). Ionic for JHipster will prompt you for the location of your backend application. Use mobile for your app’s name and app for the JHipster app’s location.

Type a when prompted to overwrite mobile/src/app/app.component.ts.

Open mobile/src/app/auth/auth.service.ts in an editor, search for data.clientId and replace it with the client ID from your Native app on Okta.

// try to get the oauth settings from the server
this.requestor.xhr({method: 'GET', url: AUTH_CONFIG_URI}).then(async (data: any) => {
  this.authConfig = {
    identity_client: '{yourClientId}',
    identity_server: data.issuer,
    redirect_url: redirectUri,
    end_session_redirect_url: logoutRedirectUri,
    scopes,
    usePkce: true
  };
  ...
}

When using Keycloak, this change is not necessary.### Add Claims to Access Token

In order to authentication successfully with your Ionic app, you have to do a bit more configuration in Okta. Since the Ionic client will only send an access token to JHipster, you need to 1) add a groups claim to the access token and 2) add a couple more claims so the user’s name will be available in JHipster.

Navigate to API > Authorization Servers, click the Authorization Servers tab and edit the default one. Click the Claims tab and Add Claim. Name it "groups" and include it in the Access Token. Set the value type to "Groups" and set the filter to be a Regex of .*. Click Create.

Add another claim, name it given_name, include it in the access token, use Expression in the value type, and set the value to user.firstName. Optionally, include it in the profile scope. Perform the same actions to create a family_name claim and use expression user.lastName.

When you are finished, your claims should look as follows.

Run the following commands to start your Ionic app.

cd mobile
ionic serve

You’ll see a screen with a sign-in button. Click on it, and you’ll be redirected to Okta to authenticate.

Now that you having log in working, you can use the entity generator to generate Ionic pages for your data model. Run the following commands (in your ~/mobile directory) to generate screens for your entities.

yo jhipster-ionic:entity album

When prompted to generate this entity from an existing one, type Y. Enter ../app as the path to your existing application. When prompted to regenerate entities and overwrite files, type Y. Enter a when asked about conflicting files.

Go back to your browser where your Ionic app is running (or restart it if you stopped it). Click on Entities on the bottom, then Albums. Click the blue + icon in the bottom corner, and add a new album.

Click the ✔️ in the top right corner to save your album. You’ll see a success message and it listed on the next screen.

Refresh your JHipster app’s album list and you’ll see it there too!

Generate code for the other entities using the following commands and the same answers as above.

yo jhipster-ionic:entity photo
yo jhipster-ionic:entity tag

Run Your Ionic App on iOS

To generate an iOS project for your Ionic application, run the following command:

ionic cordova prepare ios

When prompted to install the ios platform, type Y. When the process completes, open your project in Xcode:

open platforms/ios/MyApp.xcworkspace

You’ll need to configure code signing in the General tab, then you should be able to run your app in Simulator.

Log in to your Ionic app, tap Entities and view the list of photos.

Add a photo in the JHipster app at <a href="http://localhost:8080" target="_blank">http://localhost:8080</a>.

To see this new album in your Ionic app, pull down with your mouse to simulate the pull-to-refresh gesture on a phone. Looky there - it works!

There are some gestures you should know about on this screen. Clicking on the row will take you to a view screen where you can see the photo’s details. You can also swipe left to expose edit and delete buttons.

Run Your Ionic App on Android

Deploying your app on Android is very similar to iOS. In short:

  1. Make sure you’re using Java 8
  2. Run ionic cordova prepare android
  3. Open platforms/android in Android Studio, upgrade Gradle if prompted
  4. Set launchMode to singleTask in AndroidManifest.xml
  5. Start your app using Android Studio
  6. While your app is starting, run adb reverse tcp:8080 tcp:8080 so the emulator can talk to JHipster
Learn More About Ionic 4 and JHipster 6

Ionic is a nice way to leverage your web development skills to build mobile apps. You can do most of your development in the browser, and deploy to your device when you’re ready to test it. You can also just deploy your app as a PWA and not both to deploy it to an app store.

JHipster supports PWAs too, but I think Ionic apps look like native apps, which is a nice effect. There’s a lot more I could cover about JHipster and Ionic, but this should be enough to get you started.

You can find the source code for the application developed in this post on GitHub at @oktadeveloper/okta-ionic4-jhipster-example.

Thank you for reading!

Building CRUD Mobile App using Ionic 4, Angular 8

Building CRUD Mobile App using Ionic 4, Angular 8

A comprehensive Ionic 4 Angular 8 tutorial, learn to build CRUD (Create, Read, Update, Delete) Mobile Apps (Android/iOS)

A comprehensive Ionic 4 Angular 8 tutorial, learn to build CRUD (Create, Read, Update, Delete) Mobile Apps (Android/iOS)

The Angular 8 just released a few weeks ago, but Ionic 4 still using Angular 7. As usual, we will start this tutorial using Ionic CLI with the new version 5.

The Ionic 4 Angular 8 application flow will look like this. It just a regular CRUD (Create, Read, Update, Delete) function.

In this tutorial, we will use more Angular 8 than Ionic 4 components itself. So, if you are new to Hybrid Mobile Apps development using Ionic and familiar with Angular then this is your easy way to implement your Angular skill for Mobile App development.

The following tools, frameworks, and modules are required for this tutorial:

Remember always use the latest Ionic 4 and Angular 8 CLI to decrease compatibility issues

Before going to the main steps, we assume that you have to install Node.js. Next, upgrade or install new Ionic 4 CLI by open the terminal or Node command line then type this command.

sudo npm install -g ionic

You will get the latest Ionic 4 CLI in your terminal or command line. Check the version by type this command.

ionic -v
5.1.0

To update the Angular CLI, type this command.

sudo npm install -g @angular/cli

Now, the Angular version should be like this.

ng version
Angular CLI: 7.3.9
Create Ionic 4 Application and Update to Angular 8

We will be using Ionic CLI to create a new Ionic 4 application. Type this command to create it.

ionic start ionic4-angular8-crud --type=angular

The created Ionic 4 application still using Angular 7.2.2, for that we have to upgrade the Angular 7 to Angular 8. Go to the newly created folder then type this command using Angular CLI.

ng update @angular/cli @angular/core

If you get dependency incompatibility like below.

Package "@ionic/angular" has an incompatible peer dependency to "zone.js" (requires "^0.8.26", would install "0.9.1").
Incompatible peer dependencies found. See above.

Uninstall then install again the required dependency version.

npm uninstall --save zone.js
npm install --save zone.js

Then run again the Angular 8 update command. Next, run the Ionic 4 and Angular 7 app for the first time, but before run as lab mode, type this command to install @ionic/lab.

npm install --save-dev @ionic/lab
ionic serve -l

Now, open the browser and you will the Ionic 4 and Angular 8 app with the iOS, Android, or Windows view. If you see a normal Ionic 4 blank application, that's mean you ready to go to the next steps.

Install Angular 8 Material and CDK

For UI, we will use Angular 8 Material and CDK. To install Angular 8 Material and CDK, simply run this command.

ng add @angular/material

Type enter or yes for every question that showed up.

? Choose a prebuilt theme name, or "custom" for a custom theme: Deep Purple/Amber  [ Preview: h
ttps://material.angular.io?theme=deeppurple-amber ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

Next, register all required Angular Material CDK components or modules to app.module.ts. Open and edit that file then add this imports.

import { DragDropModule } from '@angular/cdk/drag-drop';
import { ScrollingModule } from '@angular/cdk/scrolling';

For Angular 8 Material, we will not import here but in each Ionic 4 Page Modules. Also, modify FormsModule import to add ReactiveFormsModule.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

Register the above modules to @NgModule imports.

imports: [
  BrowserModule,
  FormsModule,
  ReactiveFormsModule,
  IonicModule.forRoot(),
  AppRoutingModule,
  BrowserAnimationsModule,
  DragDropModule,
  ScrollingModule
],
Use Dynamic Imports for Angular 8 Route Configuration

Before change the Angular 8 Route configuration, we have to add the required Ionic 4 Page Module first. Type these commands to create them.

ionic g page product-detail
ionic g page product-add
ionic g page product-edit

We just added detail, add, and edit pages because the Product list will display in the Home Page Module. Next, open src/app/app-routing.module.ts then you will see the route modified and includes the page navigation. Next, we will modify this to match the new Angular 8 feature. Replace all route constant with this constant.

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)},
  { path: 'product-detail/:id', loadChildren: () => import('./product-detail/product-detail.module').then(m => m.ProductDetailPageModule)},
  { path: 'product-add', loadChildren: () => import('./product-add/product-add.module').then(m => m.ProductAddPageModule)},
  { path: 'product-edit/:id', loadChildren: () => import('./product-edit/product-edit.module').then(m => m.ProductEditPageModule)},
];

Next, modify tsconfig.json to change module and target.

{
  "compilerOptions": {
  …
  "module": "esnext",
  "moduleResolution": "node",
  …
  "target": "es2015",
  …
},
Create Ionic 4 Angular 8 RESTful API Service

To call RESTful API we will use Ionic 4 Angular Service using HttpClientModule. So, all CRUD call handle by Ionic 4 Angular 8 service that emitted the response by Observable and RXJS. Next, open and edit src/app/app.module.ts then add these imports to register HttpClientModule and FormsModule.

import { HttpClientModule } from '@angular/common/http';

Add it to @NgModule imports after BrowserModule.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule
  ...
],

We will use type specifier to get a typed result object. For that, create a new Typescript file src/app/product.ts then add these lines of Typescript codes.

export class Product {
  _id: number;
  prod_name: string;
  prod_desc: string;
  prod_price: number;
  updated_at: Date;
}

Next, generate an Ionic 4 Angular 8 service by typing this command.

ionic g service api

Next, open and edit src/app/api.service.ts then add these imports.

import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
import { Product } from './product';

Add these constants before the @Injectable.

const httpOptions = {
  headers: new HttpHeaders({'Content-Type': 'application/json'})
};
const apiUrl = 'http://localhost:3000/api/v1/products';

Inject HttpClient module to the constructor.

constructor(private http: HttpClient) { }

Add the error handler function.

private handleError(operation = 'operation', result?: T) {
  return (error: any): Observable => {
    console.error(error); // log to console instead
    return of(result as T);
  };
}

Add all CRUD (create, read, update, delete) functions of products data.

getProducts(): Observable {
  return this.http.get(apiUrl)
    .pipe(
      tap(product => console.log('fetched products')),
      catchError(this.handleError('getProducts', []))
    );
}

getProduct(id: any): Observable {
  const url = `${apiUrl}/${id}`;
  return this.http.get(url).pipe(
    tap(_ => console.log(`fetched product id=${id}`)),
    catchError(this.handleError(`getProduct id=${id}`))
  );
}

addProduct(product: Product): Observable {
  return this.http.post(apiUrl, product, httpOptions).pipe(
    tap((prod: Product) => console.log(`added product w/ id=${prod._id}`)),
    catchError(this.handleError('addProduct'))
  );
}

updateProduct(id: any, product: any): Observable {
  const url = `${apiUrl}/${id}`;
  return this.http.put(url, product, httpOptions).pipe(
    tap(_ => console.log(`updated product id=${id}`)),
    catchError(this.handleError('updateProduct'))
  );
}

deleteProduct(id: any): Observable {
  const url = `${apiUrl}/${id}`;

  return this.http.delete(url, httpOptions).pipe(
    tap(_ => console.log(`deleted product id=${id}`)),
    catchError(this.handleError('deleteProduct'))
  );
}
View List of Data

As we mention in the begining of this article, we will use existing Ionic 4 Home Page Module to display list of data. For that, open and edit src/app/home/home.page.ts then add/replace these imports.

import { Component, OnInit } from '@angular/core';
import { LoadingController } from '@ionic/angular';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Product } from '../product';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';

Next, add Angular 8 OnInit implementation to the HomePage Class name.

export class HomePage implements OnInit

Next, add the constructor then inject those modules to the constructor.

constructor(
  public api: ApiService,
  public loadingController: LoadingController,
  public router: Router,
  public route: ActivatedRoute) { }

Remove all default generated variable, function and constructor body if exists then add this variable before the constructor for hold classroom data that get from the service.

products: Product[] = [];

Add function for getting Product list from API.

async getProducts() {
  const loading = await this.loadingController.create({
    message: 'Loading...'
  });
  await loading.present();
  await this.api.getProducts()
    .subscribe(res => {
      this.products = res;
      console.log(this.products);
      loading.dismiss();
    }, err => {
      console.log(err);
      loading.dismiss();
    });
}

Add Angular 8 init function after the constructor for call above function.

ngOnInit() {
  this.getProducts();
}

Add function for the new Angular 8 CDK Drag&Drop.

drop(event: CdkDragDrop) {
  moveItemInArray(this.products, event.previousIndex, event.currentIndex);
}

Next, because we will use the new Angular 8 CDK features. We should add modules for it to src/app/home/home.module.ts then add these imports.

import { ScrollingModule } from '@angular/cdk/scrolling';
import { DragDropModule } from '@angular/cdk/drag-drop';

Register to @NgModule imports array.

imports: [
  IonicModule,
  CommonModule,
  FormsModule,
  ScrollingModule,
  DragDropModule,
  RouterModule.forChild([{ path: '', component: HomePage }])
],

Next, open and edit src/app/home/home.page.html then replace all HTML tags with this.


  
    Home
  



  
    
      
      {{p.prod_name}}
      
        {{p.prod_price | currency}}
      
    
  

Finally, give this page a style by open and edit src/app/home/home.page.scss then replace all SCSS codes with these.

.example-viewport {
  height: 100%;
  width: 100%;
  border: none;
}

.example-item {
  min-height: 50px;
}
View Data Details and Add Delete Function

Every time you click the list item in the List of data, you will be redirected to Details tab including the ID of the selected data. Open and edit src/app/product-detail/product-detail.page.ts then add/replace this imports.

import { Component, OnInit } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { ApiService } from '../api.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Product } from '../product';

Inject above modules to the constructor.

constructor(
  public api: ApiService,
  public alertController: AlertController,
  public route: ActivatedRoute,
  public router: Router) {}

Add the variables before the constructor for hold Product data and Angular 8 Loading Spinner.

product: Product = { _id: null, prod_name: '', prod_desc: '', prod_price: null, updated_at: null };
isLoadingResults = false;

Add an asynchronous function to getting Product detail from API.

async getProduct() {
  if (this.route.snapshot.paramMap.get('id') === 'null') {
    this.presentAlertConfirm('You are not choosing an item from the list');
  } else {
    this.isLoadingResults = true;
    await this.api.getProduct(this.route.snapshot.paramMap.get('id'))
      .subscribe(res => {
        console.log(res);
        this.product = res;
        this.isLoadingResults = false;
      }, err => {
        console.log(err);
        this.isLoadingResults = false;
      });
  }
}

Add an asynchronous function for display an alert.

async presentAlertConfirm(msg: string) {
  const alert = await this.alertController.create({
    header: 'Warning!',
    message: msg,
    buttons: [
      {
        text: 'Okay',
        handler: () => {
          this.router.navigate(['']);
        }
      }
    ]
  });

  await alert.present();
}

Call get product function from Angular 8 init function.

ngOnInit() {
  this.getProduct();
}

Add the functions to delete the data.

async deleteProduct(id: any) {
  this.isLoadingResults = true;
  await this.api.deleteProduct(id)
    .subscribe(res => {
      this.isLoadingResults = false;
      this.router.navigate([ '/home' ]);
    }, err => {
      console.log(err);
      this.isLoadingResults = false;
    });
}

Add a function to navigate to the Edit Product page.

editProduct(id: any) {
  this.router.navigate([ '/product-edit', id ]);
}

Next, open and edit src/app/details/details.page.html then replace all HTML tags with this.


  
    
      
    
    Product Details
  



  
    <div class="example-loading-shade"
          *ngIf="isLoadingResults">
      
    
    
      
        ## {{product.prod_name}}

        {{product.prod_desc}}
      
      
        
          Product Price:
          {{product.prod_price}}
          Updated At:
          {{product.updated_at | date}}
        
      
      
        edit
        delete
      
    
  

Finally, give this page a style by open and edit src/app/product-detail/product-detail.page.scss then replace all SCSS codes with these.

.example-container {
  position: relative;
  padding: 5px;
  height: 100%;
  background-color: aqua;
}

.example-loading-shade {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 56px;
  right: 0;
  background: rgba(0, 0, 0, 0.15);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.mat-flat-button {
  margin: 5px;
}
Create a Form to Add Data using Angular 8 Material

To create a form for adding a Product Data using Angular 8 Material, open and edit src/app/product-add/product-add.page.ts then add these imports.

import { Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';

Inject above modules to the constructor.

constructor(
  private router: Router,
  private api: ApiService,
  private formBuilder: FormBuilder) { }

Declare variables for the Form Group and all of the required fields inside the form before the constructor.

productForm: FormGroup;
prod_name = '';
prod_desc = '';
prod_price: number = null;
isLoadingResults = false;

Add initial validation for each field in the ngOnInit function.

this.productForm = this.formBuilder.group({
  'prod_name' : [null, Validators.required],
  'prod_desc' : [null, Validators.required],
  'prod_price' : [null, Validators.required]
});

Create a function for submitting or POST product form.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.addProduct(this.productForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/product-details', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
}

Next, add this import for implementing ErrorStateMatcher.

import { ErrorStateMatcher } from '@angular/material/core';

Create a new class before the main class @Components.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Instantiate that MyErrorStateMatcher as a variable in the main class.

matcher = new MyErrorStateMatcher();

Before modifying the HTML file, we have to register all Angular 8 Material files by open and edit src/app/product-add/product-add.module.ts then add these imports.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule } from '@angular/material';

Declare that imported modules to then @NgModule imports array.

imports: [
  ...
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, open and edit src/app/product-add/product-add.component.html then replace all HTML tags with this.


  
    
      
    
    Product Add
  



  
    <div class="example-loading-shade"
         *ngIf="isLoadingResults">
      
    
    
      
        
          <input matInput placeholder="Product Name" formControlName="prod_name"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Name
          
        
        
          <input matInput placeholder="Product Desc" formControlName="prod_desc"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Description
          
        
        
          <input matInput placeholder="Product Price" formControlName="prod_price"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Price
          
        
        
          save
        
      
    
  

Finally, open and edit src/app/product-add/product-add.component.scss then add this SCSS codes.

.example-container {
  position: relative;
  padding: 5px;
  height: 100%;
  background-color: aqua;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}

.example-card {
  margin: 5px;
}
Create a Form to Edit Data using Angular 8 Material

To create a Form of Edit Product Data using Angular 8 Material, open and edit src/app/product-edit/product-edit.page.ts then add these lines of imports.

import { Router, ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Add a new Class before the @Component that handles the error message in the HTML form.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Next, add these lines of variables to the main Class before the constructor.

productForm: FormGroup;
_id = '';
prod_name = '';
prod_desc = '';
prod_price: number = null;
isLoadingResults = false;
matcher = new MyErrorStateMatcher();

Inject the constructor params with these modules.

constructor(
  private router: Router,
  private route: ActivatedRoute,
  private api: ApiService,
  private formBuilder: FormBuilder) { }

Initialize the form group with the form controls of the product form and call the product detail data in the Angular 8 ngOnInit function.

ngOnInit() {
  this.getProduct(this.route.snapshot.params['id']);
  this.productForm = this.formBuilder.group({
    'prod_name' : [null, Validators.required],
    'prod_desc' : [null, Validators.required],
    'prod_price' : [null, Validators.required]
  });
}

Create a new Angular 8 function to call the product data by ID.

getProduct(id: any) {
  this.api.getProduct(id).subscribe((data: any) => {
    this._id = data._id;
    this.productForm.setValue({
      prod_name: data.prod_name,
      prod_desc: data.prod_desc,
      prod_price: data.prod_price
    });
  });
}

Create a new Angular 8 function that handles the form submission to save data to the REST API.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.updateProduct(this._id, this.productForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/product-details', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      }
    );
}

Add a function to navigate to the Product Detail page.

productDetails() {
  this.router.navigate(['/product-details', this._id]);
}

Before modifying the HTML file, we have to register all Angular 8 Material files by open and edit src/app/product-edit/product-edit.module.ts then add these imports.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule } from '@angular/material';

Declare that imported modules to then @NgModule imports array.

imports: [
  ...
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, open and edit src/app/product-edit/product-edit.page.html then replace all HTML tags with these.


  
    
      
    
    Product Edit
  



  
    <div class="example-loading-shade"
         *ngIf="isLoadingResults">
      
    
    
      
        
          <input matInput placeholder="Product Name" formControlName="prod_name"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Name
          
        
        
          <input matInput placeholder="Product Desc" formControlName="prod_desc"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Description
          
        
        
          <input matInput placeholder="Product Price" formControlName="prod_price"
                 [errorStateMatcher]="matcher">
          
            Please enter Product Price
          
        
        
          save
        
      
    
  

Finally, add some styles for this page by open and edit src/app/product-edit/product-edit.page.scss then replace all SCSS codes with these.

.example-container {
  position: relative;
  padding: 5px;
  height: 100%;
  background-color: aqua;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}

.example-card {
  margin: 5px;
}
Run and Test the Whole Ionic 4 Angular 8 Mobile Apps

Before running Ionic 4 Angular 8 Mobile Apps we have to start the MongoDB server and Node/Express.js REST API server. Type these commands in the separate Terminal/CMD tabs.

mongod
nodemon

Now, we have to run the Ionic 4 Angular 8 Mobile Apps in the browser using this command.

ionic serve -l

And here we go, the full Ionic 4 Angular 8 Mobile Apps CRUD functions.

That it's, the comprehensive step by step tutorial of Ionic 4 Angular 8 CRUD Mobile Apps. You can find the full source code from our GitHub.

How to Turn an Angular app into a PWA

How to Turn an Angular app into a PWA

Turn your Angular App into a PWA in Easy Steps

Progressive Web Apps (PWAs) are web apps that aim to offer an experience similar to a native, installed application. They use service workers to cache front-end files and back-end information so they can function faster and even work offline (at least partially), add a web manifest to allow users to install the front-end on their device like any other app and even implement push notifications, all to offer an experience closer to an native app than what is expected from a “normal” website.

Angular makes it easy to fulfill the bare minimum requirements for a web app to be considered a PWA, but optimizing to deliver a truly good PWA requires a bit more work. In this article, I will try to walk you through this process.

PWA requirements

Defining a PWA just as a web app that tightens the gap between web based and native app is a bit broad. Google offers a more specific definition, providing a checklist of things they consider as minimum requirements for a web app to be considered a PWA, and also a list for those who want to implement an exemplary PWA.

There’s also a tool called Lighthouse, which is bundled with Chrome for desktop, that can help a lot in checking if your web app complies to the PWA requirements. You can access it by opening the developer tools and going to the “Audits” tab.

If you check Progressive Web App and run an audit right now most tests will probably fail.

Don’t let that discourage you, as Angular makes it quite easy to go from that to passing most tests.

Making your Angular app a PWA

The @angular/pwa npm package performs many of the steps necessary to make your Angular app a PWA. When added to your project, it will set up a service worker, add a web manifest, add icons and a theme color, and add a tag at index.html to show some content when the JavaScript code from your app hasn’t loaded (probably either because the user has a very slow connection or because their browser can’t run Angular).

To add @angular/pwa to your project, type this on a console in your project’s folder:

ng add @angular/pwa

If you run your app with ng serve and try to audit it now, many tests will be successful, but you will probably notice it still fails tests related to service workers.

If you go to the “Application” tab of the developer tools on Google Chrome (or similar tools on other browsers) you will see a service worker running, but with an error message. That’s because ng serve doesn’t work well with service workers, and it’s necessary to build your app and run through a server to make it work.

One easy way of doing this is to use the http-server npm pack. You can use the following code on a console to build your app, install http-server and run a server with it.

ng build --prod
npm install http-server -g
http-server -p 8080 -c-1 dist/<project-name>

You can access your web app at http://127.0.0.1:8080. If you audit it now it will pass all but one test (the one about redirecting HTTP to HTTPS), but you should probably only worry about this one on your production environment. Also worth noting it passes the test about running your website with HTTPS even if it isn’t just because you are running it locally. The test actually works otherwise, and will fail if the website isn’t running with HTTPS.

Optimizations

Your Angular app now has the bare minimum to be considered a PWA, and that’s actually pretty good already. If you reload your page you will notice it loads really fast thanks to the service worker caching the front-end static files, and when you deploy your app to an environment running with HTTPS, a user will be prompted to install your app.

That doesn’t mean there isn’t room for improvement.

Theme

The more obvious improvement would be to, first of all, override the “theme” applied by @angular/pwa to your project, by replacing all icons created by different sizes of your own icons, and by picking your own color for the theme-color at /src/index.html and at /src/manifest.webmanifest.

Cache

Another not so obvious yet significant optimization is to set up caching of back-end information. To do so you will need to divide your endpoints into “data groups”, which define how they will be cached.

The most important option of a data group is probably deciding between the two available strategies, “performance” and “freshness”. Performance will use cached information whenever available, while freshness always tries to fetch information from the internet, falling back to cache when there’s no connection to the internet.

It’s also possible to control how long responses stay in cache before they are discarded, and to set a limit to how large responses can be in order to be cached.

Properly using these options may greatly improve the overall performance of your web app, but misusing them may lead to showing old data to a user without their knowledge. For some types of data speed is preferable over always having the most updated data (non-critical data that rarely changes, like, for example, the link to a user profile photo shown at the navbar), but showing old data can be really bad depending on the kind of data being handled (if you are a bank showing outdated bank transactions data, you may have problems), so be careful.

In order to set up a data group, open the ngsw-config.json file (created when the pwa pack was added to the project) and add a dataGroups key to the json object. This key should contain an array of objects, each defining a name, an array of urls and a cacheConfig object.

You can check the documentation for more details. The following is a sample of how to set up a data group:

"dataGroups": [{
  "name": "api-cache",
  "urls": [ "/test1", "/test2/*" ],
  "cacheConfig": {
    "strategy": "freshness",
    "maxSize": 131072,
    "maxAge": "1d",
    "timeout": "15s"
  }
}]

Worth noting, caching responses only works for HTTP methods that don’t (or shouldn’t) make any changes on back-end information, so POSTs, DELETEs and such methods do not have their responses cached.

Handling different versions

By default, an Angular PWA will load front-end files from cache whenever available, and if there’s an active internet connection newer versions will be downloaded for the next time the users visit your website.

That means faster loading times, which is very good, but this also means a lot of users will use old versions for a while. Depending of what kind of website you have and what kind of changes you made on theses newer versions this could not make much difference, but this could also be very, very bad.

The good news is it’s not so hard to circumvent this problem, as the SwUpdate class was made for it. It provides us with two observables to listen to when updates are made available and when an update has been applied, and two promises to manually check availability and apply updates.

Those can be used to warn the user about a newer version and allow them to either continue using the current one or to reload the page and use the newer one. You could add something like this to your app.component.ts to achieve this behavior:

constructor(swUpdate: SwUpdate) { }
ngOnInit() {
  this.swUpdate.available
    .subscribe(update => this.newVersion = true);
}
// Make a button that only appears when newVersion and use this function as its action
reload() {
  this.swUpdate.activated
    .subscribe(update => window.location.reload(true));
  this.swUpdate.activateUpdate();
}

Modify this to let the user know there’s a new version (in this code, when newVersion is set to true). How exactly you do this depends on your web app’s design, but a dialog/ modal or a toast / snackbar is probably a good way to go.

As a heads up, SwUpdate.available takes a few seconds to fire after the page loads, and it will only fire the first time you boot the website after an update. This makes developing this functionality a bit harder, but if you keep in mind how things work there shouldn’t be any problem.

User experience

Up until now you have probably designed your web app with the mindset that if your user isn’t online then they can’t really access your web app. It would be a great idea to make sure things wouldn’t just collapse on a quick connection failure, but in the end it was meant to work with an internet connection.

Now your web app is actually meant to work both online and offline, and you must make sure the experience remains consistent all the time. Even further, you need to make sure your web app “feels” like a native, installed app.

There are many tweaks you could do to improve this. Some of these are actually good practice for pretty much every website, but are particularly important for a PWA.

  • Responsiveness nowadays is important for any website, but it’s extra important for one trying to pass for a native app. If your UI doesn’t feel like it was optimized for a user’s screen size it won’t feel like a native app, and it’s specially bad if they have to keep zooming in/ out to properly use it.
  • If your website has some real time features it’s probably a good idea to inform your users if they are offline (either by checking from time to time or by using the Network Information API on supporting browsers).
  • Don’t let page transitions feel like your web app is blocked. If a page transition depends on data being transmitted (like when a form is being submitted) show a loading indicator, and if it doesn’t need anything just show the next page instantly (Angular apps are single page applications, make good use of that).
  • If a part of a page is waiting for some data to download, make sure it won’t keep “jumping” while data is fetched. Give a fixed height for image containers and show skeleton screens (or at least some simple loading indicator) where applicable . For faster back-end services, you may want to use resolve guards to further the illusion of a native app and not a website being built as data is received.
  • If a request failed, show a retry component (specially important for users with poor connection, which is common for mobile users) or, if not possible for some reason, at least show an error message.
Notifications

This would be a lengthy subject and deserve an article of its own. I will at least recommend Angular University blog’s article about notifications on Angular.

Further improvements

With all of that, your Angular PWA should offer a great user experience, with good performance and a native app-like experience.

I will not cover these in detail on this article, but Google suggests even more improvements to further improve the user experience in your PWA, like making sure that your website can be indexed by search engines and that it looks good when shared on social media.

Angular apps being single page apps has its advantages, but one downside is the fact it’s harder for search engines and social networks to crawl Angular apps. That’s not to say it can’t be done, just that it’s not so easy. Look up for “Angular SEO” and you will probably find many articles on the subject. Depending on the complexity of your website you may need to look for “Angular Universal”, which implements server-side rendering on Angular apps.

Little things, like making sure an input won’t be hidden by a mobile on-screen keyboard, or implementing scroll history (when pressing back, making the scroll position the same as when the user left the page) are also a nice touch.

Lastly, be mindful of how you handle push notifications and install notifications. Make it too little and you will be losing a great user engagement opportunity, make it too much and you will annoy your users. You probably don’t want to annoy your users.