Build Your First PWA with Angular

Build Your First PWA with Angular

Learn how to build an Angular PWA (Progressive Web Apps) from scratch in this free tutorial.

Learn how to build an Angular PWA (Progressive Web Apps) from scratch in this free tutorial.

During the last two years, everybody started talking about Progressive Web Applications, or PWAs for short. But what is this new type of application, and how can it make your life as an Angular developer better? To understand what PWAs are all about, and how you can build them in Angular, let’s consider the following scenario. You are out and about in an area with little or no network reception. You are using a cool web app to search for a good book to read. Traditional web applications only work while you are online. Every time you lose the network the application will stall. What’s more, a typical app will load all of its scripts before starting up. This means that you might have to wait a minute or more for the first page to load. In conditions like this, you will quickly give up and abandon the application altogether.

This is where progressive web applications come into play. PWAs leverage a number of current browser technologies in order to provide a smooth user experience even in situations with little or no network connection. They use service workers which act a little like a proxy to intercept network requests and cache the responses. They allow the complete application to be installed in the client’s browser. This means that the user can use the application when they’re offline.

In this tutorial, I will show you how to create a complete progressive web app in Angular 7. The application will allow the user to search for book titles using the OpenLibrary service. I will be using a Material Design library, Angular Material, to give the application layout a professional appearance and make it responsive.

Create Your Single Page Application with Angular

Start by creating a single page application with Angular 7. I will assume that you have Node installed on your system. To begin you will need to install the Angular command line tool. Open a shell and enter the following command.

npm install -g @angular/[email protected]


This will install the ng command on your system. Depending on your system settings you might need to run this command using sudo. Once npm has finished installing, you’ll be ready to create a new Angular project. In the shell, navigate to the directory in which you want to create your application and type the following command.

ng new AngularBooksPWA


This will create a new directory called AngularBooksPWA and create an Angular application in it. The script will ask you two questions. When you are asked if you want to use the Router in your project, answer Yes. The router will allow you to navigate between different application components using the browser’s URL. Next, you are going to be prompted for the CSS technology that you wish to use. In this simple project, I will be using plain CSS. For larger projects, you should switch this to one of the other technologies. Once you have answered the questions ng will install all the necessary packages into the newly created application directory and create a number of files to help you get started quickly.

Add Angular Material

Next, navigate into your project’s directory and run the following command.

npm install @angular/[email protected] @angular/[email protected] @angular/[email protected] @angular/[email protected]


This command will install all the necessary packages for using Material Design. Material design uses an icon font to display icons. This font is hosted on Google’s CDN. To include the icon font, open the src/index.html file and add the following line inside the tags.

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">


The src/app/app.module.ts contains the imports for the modules which will be available throughout the application. In order to import the Angular Material modules that you will be using, open the file and update it to match the following.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from "@angular/flex-layout";

import { MatToolbarModule,
         MatMenuModule,
         MatIconModule,
         MatCardModule,
         MatButtonModule,
         MatTableModule,
         MatDividerModule,
         MatProgressSpinnerModule } from '@angular/material';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    FlexLayoutModule,
    FormsModule,
    ReactiveFormsModule,
    MatToolbarModule,
    MatMenuModule,
    MatIconModule,
    MatCardModule,
    MatButtonModule,
    MatTableModule,
    MatDividerModule,
    MatProgressSpinnerModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


The template for the main component of the application lives in the src/app/app.component.html file. Open this file and replace the contents with the following code.

<mat-toolbar color="primary" class="expanded-toolbar">
    <span>
      <button mat-button routerLink="/">{{title}}</button>
      <button mat-button routerLink="/"><mat-icon>home</mat-icon></button>
    </span>
    <div fxLayout="row" fxShow="false" fxShow.gt-sm>
        <form [formGroup]="searchForm" (ngSubmit)="onSearch()">
          <div class="input-group">
            <input class="input-group-field" type="search" value="" placeholder="Search" formControlName="search">
            <div class="input-group-button"><button mat-flat-button color="accent"><mat-icon>search</mat-icon></button></div>
          </div>
        </form>
    </div>
    <button mat-button [mat-menu-trigger-for]="menu" fxHide="false" fxHide.gt-sm>
     <mat-icon>menu</mat-icon>
    </button>
</mat-toolbar>
<mat-menu x-position="before" #menu="matMenu">
    <button mat-menu-item routerLink="/"><mat-icon>home</mat-icon> Home</button>

    <form [formGroup]="searchForm" (ngSubmit)="onSearch()">
      <div class="input-group">
        <input class="input-group-field" type="search" value="" placeholder="Search" formControlName="search">
        <div class="input-group-button"><button mat-button routerLink="/"><mat-icon>magnify</mat-icon></button></div>
      </div>
    </form>
</mat-menu>
<router-outlet></router-outlet>


You might notice the routerLink attributes used in various places. These refer to components that will be added later in this tutorial. Also, note the HTML tag and the formGroup attribute. This is the search form that will allow you to search for book titles. I will be referring to this when implementing the application component.

Next, I will add a bit of styling. Angular separates the style sheets into a single global style sheet and local style sheets for each component. First, open the global style sheet in src/style.css and paste the following content into it.

@import "[email protected]/material/prebuilt-themes/deeppurple-amber.css";

body {
  margin: 0;
  font-family: sans-serif;
}

h1, h2 {
  text-align: center;
}

.input-group {
  display: flex;
  align-items: stretch;
}

.input-group-field {
  margin-right: 0;
}

.input-group .input-group-button {
  margin-left: 0;
  border: none;
}

.input-group .mat-flat-button {
  border-radius: 0;
}


The first line in this style sheet in necessary to apply the correct styles to any Angular Material components. The local style for the main application component is found in src/app/app.component.css. Add the toolbar styling here.

.expanded-toolbar {
  justify-content: space-between;
  align-items: center;
}


Add a Search Feature with Angular

Now, you are finally ready to implement the main application component. Open src/app/app.component.ts and replace its content with the following.

import { Component } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from "@angular/router";

import { BooksService } from './books/books.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'AngularBooksPWA';
  searchForm: FormGroup;

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

  ngOnInit() {
    this.searchForm = this.formBuilder.group({
      search: ['', Validators.required],
    });
  }

  onSearch() {
    if (!this.searchForm.valid) return;
    this.router.navigate(['search'], { queryParams: {query: this.searchForm.get('search').value}});
  }
}


There are two things to note about this code. The searchForm attribute is a FormGroup which is created using the FormBuilder. The builder allows the creation of form elements that can be associated with validators to allow easy validation of any user input. When the user submits the form, the onSearch() function is called. This checks for valid user input and then simply forwards the call to the router. Note how the query string is passed to the router. This will append the query to the URL and make it available to the search route. The router will pick the appropriate component and the book search is handled within that component. This means that the responsibility for performing the search request is encapsulated in the local scope of a single component. When building larger applications, this separation of responsibilities is an important technique to keep the code simple and maintainable.

Create a BookService to talk to the OpenLibrary API

Next, create a service that will provide a high-level interface to the OpenLibrary API. To have Angular create the service, open the shell again in the application root directory and run the following command.

ng generate service books/books


This will create two files in the src/app/books directory. Open the books.service.ts file and replace its contents with the following.

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

const baseUrl = 'http://openlibrary.org';

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

  constructor(private http: HttpClient) { }

  async get(route: string, data?: any) {
    const url = baseUrl+route;
    let params = new HttpParams();

    if (data!==undefined) {
      Object.getOwnPropertyNames(data).forEach(key => {
        params = params.set(key, data[key]);
      });
    }

    const result = this.http.get(url, {
      responseType: 'json',
      params: params
    });

    return new Promise<any>((resolve, reject) => {
      result.subscribe(resolve as any, reject as any);
    });
  }

  searchBooks(query: string) {
    return this.get('/search.json', {title: query});
  }
}


To keep things simple, you can use a single route into the OpenLibrary API. The search.json route takes a search request and returns a list of books together with some information about them. Note how the functions return a Promise object. This will make it easier to use them later on in using the async/await technique.

Generate Angular Components for Your PWA using Angular CLI

Now it’s time to turn your attention to the components that make up the books search application. There will be three components in total. The Home component displays the splash screen, Search lists the book search results and Details displays detailed information about a single book. To create these components, open the shell and execute the following commands.

ng generate component home
ng generate component search
ng generate component details


After having created these three components, you have to link them to specific routes using the Router. Open src/app/app-routing.module.ts and add routes for the components you just created.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { SearchComponent } from './search/search.component';
import { DetailsComponent } from './details/details.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'search', component: SearchComponent },
  { path: 'details', component: DetailsComponent },
];

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


Start with the Home component. This component will consist only of two simple headings. Open src/app/home/home.component.html and enter the lines below.

<h1>Angular Books PWA</h1>
<h2>A simple progressive web application</h2>


Next, implement the search component by changing the code in src/app/search/search.component.ts to look like the following.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatTableDataSource } from '@angular/material';
import { BooksService } from '../books/books.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {
  private subscription: Subscription;

  displayedColumns: string[] = ['title', 'author', 'publication', 'details'];
  books = new MatTableDataSource<any>();

  constructor(private route: ActivatedRoute,
              private router: Router,
              private bookService: BooksService) { }

  ngOnInit() {
    this.subscription = this.route.queryParams.subscribe(params => {
      this.searchBooks(params['query']);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  async searchBooks(query: string) {
    const results = await this.bookService.searchBooks(query);

    this.books.data = results.docs;
  }

  viewDetails(book) {
    console.log(book);
    this.router.navigate(['details'], { queryParams: {
      title: book.title,
      authors: book.author_name && book.author_name.join(', '),
      year: book.first_publish_year,
      cover_id: book.cover_edition_key
    }});
  }
}


There are a few things going on here. During initialization of the component, the search query is obtained by subscribing to the ActivatedRoute.queryParams Observable. Whenever the value changes, this will call the searchBooks method. Inside this method, the BooksService, which you implemented earlier, is used to obtain a list of books. The result is passed to a MatTableDataSource object that allows displaying beautiful tables with the Angular Material library.

Take a look at the src/app/search/search.component.html and update its HTML to match the template below.

<h1 class="h1">Search Results</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="products">
  <table mat-table fxFlex="100%" fxFlex.gt-sm="66%" [dataSource]="books" class="mat-elevation-z1">
    <ng-container matColumnDef="title">
      <th mat-header-cell *matHeaderCellDef>Title</th>
      <td mat-cell *matCellDef="let book"> {{book.title}} </td>
    </ng-container>
    <ng-container matColumnDef="author">
      <th mat-header-cell *matHeaderCellDef>Author</th>
      <td mat-cell *matCellDef="let book"> {{book.author_name && book.author_name.join(', ')}} </td>
    </ng-container>
    <ng-container matColumnDef="publication">
      <th mat-header-cell *matHeaderCellDef>Pub. Year</th>
      <td mat-cell *matCellDef="let book"> {{book.first_publish_year}} </td>
    </ng-container>
    <ng-container matColumnDef="details">
      <th mat-header-cell *matHeaderCellDef></th>
      <td mat-cell *matCellDef="let book">
        <button mat-icon-button (click)="viewDetails(book)"><mat-icon>visibility</mat-icon></button>
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</div>


This table uses the data source to display the search results. The last component displays the details of the book, including its cover image. Just like the Search component, the data is obtained through subscribing to the route parameters. Open src/app/details/details.component.ts and update the content to the following.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent implements OnInit {
  private subscription: Subscription;
  book: any;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.subscription = this.route.queryParams.subscribe(params => {
      this.updateDetails(params);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  updateDetails(book) {
    this.book = book;
  }
}


The template simply shows some of the fields in the book’s data structure. Copy the following into src/app/details/details.component.html.

<h1 class="h1">Book Details</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="products">
  <mat-card fxFlex="100%" fxFlex.gt-sm="66%" class="mat-elevation-z1">
    <h3>{{book.title}}</h3>
    <img src="http://covers.openlibrary.org/b/OLID/{{book.cover_id}}-M.jpg" />
    <h4>Authors</h4>
    <p>
      {{book.authors}}
    </p>
    <h4>Published</h4>
    <p>
      {{book.year}}
    </p>
  </mat-card>
</div>


Run Your Angular PWA

The application is now complete. You can now start up and test the application. When building a PWA, you should not use the ng serve command to run your application. This is OK during development but it will disable a number of features that are necessary for the performance of PWAs. Instead, you need to build the application in production mode and the serve it using the http-server-spa command. Run the following commands.

npm install -g [email protected]
ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


You need to run the first command only once to install the http-server-spa command. The second line builds your Angular app. With the --source-map option you will generate source maps that help you debugging in the browser. The last command starts the HTTP server. Open your browser, navigate to http://localhost:8080, and enter a book title in the search bar. You should see a list of books, somewhat like this.

Add Authentication to your Angular PWA

A complete application will have to have some user authentication to restrict access to some of the information contained within the application. Okta allows you to implement authentication in a quick, easy, and safe way. In this section I will show you how to implement authentication using the Okta libraries for Angular. If you haven’t done so already, register a developer account with Okta.

Open your browser and navigate to developer.okta.com. Click on Create Free Account. On the next screen enter your details and click Get Started. You will be taken to your Okta developer dashboard. Each application that uses the Okta authentication service needs to be registered in the dashboard. Click on Add Application to create a new application.

The PWA that you are creating falls under the single page application category. Choose Single Page App and click Next.

On the next page, you will be shown the settings for the application. You can leave the default settings untouched and click on Done. On the following screen you will be presented with a Client ID. This is needed in your application.

To add authentication to your PWA, first install the Okta library for Angular.

npm install @okta/[email protected] --save-exact


Open app.module.ts and import the OktaAuthModule.

import { OktaAuthModule } from '@okta/okta-angular';


Add the OktaAuthModule to the list of imports of the app.

OktaAuthModule.initAuth({
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  redirectUri: 'http://localhost:8080/implicit/callback',
  clientId: '{yourClientId}'
})


The {yourClientId} has to be replaced by the client ID that you obtained when registering your application. Next, open app.component.ts, and import the service.

import { OktaAuthService } from '@okta/okta-angular';


Create an isAuthenticated field as a property of the AppComponent.

isAuthenticated: boolean;


Then, modify the constructor to inject the service and subscribe to it.

constructor(private formBuilder: FormBuilder,
            private router: Router,
            public oktaAuth: OktaAuthService) {
  this.oktaAuth.$authenticationState.subscribe(
    (isAuthenticated: boolean)  => this.isAuthenticated = isAuthenticated
  );
}


Whenever the authentication status changes this will be reflected in the isAuthenticated property. You will still need to initialize it when the component is loaded. In the ngOnInit() method add the line

this.oktaAuth.isAuthenticated().then((auth) => {this.isAuthenticated = auth});


You want the application to be able to react to login and logout requests. To do this, implement the login() and logout() methods as follows.

login() {
  this.oktaAuth.loginRedirect();
}

logout() {
  this.oktaAuth.logout('/');
}


Open app.component.html and add the following lines to the top bar before

.

<button mat-button *ngIf="!isAuthenticated" (click)="login()"> Login </button>
<button mat-button *ngIf="isAuthenticated" (click)="logout()"> Logout </button>


Finally, you need to register the route that will be used for the login request. Open app-routing.module.ts and add the following imports.

import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';


Add the implicit/callback route to the routes array.

{ path: 'implicit/callback', component: OktaCallbackComponent }


This is the route that the Okta authorization service will return to, once authentication is completed. The next step is to protect the search and the details routes from unauthorized access, add the following setting to both routes.

canActivate: [OktaAuthGuard]


Your routes array should look as follows after these changes.

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'search', component: SearchComponent, canActivate: [OktaAuthGuard] },
  { path: 'details', component: DetailsComponent, canActivate: [OktaAuthGuard] },
  { path: 'implicit/callback', component: OktaCallbackComponent }
];


This is it. Whenever a user tries to access the Search or Details view of the application, they will be redirected to the Okta login page. Once logged on, the user will be redirected back to the view that they wanted to see in the first place. As before, you can build and start your application by running the commands:

ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


Create a Progressive Web App with Angular

The application runs and works, but it is not a Progressive Web Application. To see how your application performs, I will be using the Lighthouse extension. If you don’t want to install Lighthouse, you can also use the audit tool built into Google Chrome. This is a slightly less up-to-date version of Lighthouse accessible through Developer Tools > Audits. Install the Lighthouse extension for the Chrome browser. This extension allows you to analyze the performance and compatibility of web pages and applications.

After installation, open your application, click on the small Lighthouse logo and run the test. At the moment, your Books application likely rates poorly on the PWA scale, achieving only 46%.

Over the last year, the folks developing Angular have made it very easy to turn your regular application into a PWA. Shut down the server and run the following command.

ng add @angular/pwa --project AngularBooksPWA


Rebuild your application, start the server, and run Lighthouse again. I tried and I got a score of 92%. The only reason that the application is not achieving 100% is due to the fact that it is not served via https protocol.

What did adding PWA support do? The most important improvement is the addition of a service worker. A service worker can intercept requests to the server and returns cached results wherever possible. This means that the application should work when you’re offline. To test this, open the developer console, open the network tab, and tick the offline check box. When you now click the reload button, the page should still work and show some content.

Adding PWA support also created application icons of various sizes (in the src/assets/icons/ directory). Naturally, you will want to replace them with your own icons. Use any regular image manipulation software to create some cool logos. Finally, a web app manifest was added to the file src/manifest.json. The manifest provides the browser with information that it needs to install the application locally on the user’s device.

Does that mean that you are done turning your application into a PWA? Not at all! There are numerous other features that are not tested by Lighthouse, but still make for a good Progressive Web Application. Check Google’s Progressive Web App Checklist for a list of features that make a good PWA.

Cache Recent Requests and Responses

Start the Books application in your browser and search for a book. Now click on the eye icon of one of the books to view its details. After the details page has loaded, open the developer console and switch to offline mode (Network tab > check Offline). In this mode click the back button in your browser. You will notice that the content has disappeared. The application is trying to request resources from the OpenLibrary API again. Ideally, you would like to keep some search results in the cache. Also, it would be nice for the user to know that they are using the application in offline mode.

I will start with the cache. The following code is adapted from Tamas Piros’ great article about caching HTTP requests. Start by creating two new services.

ng generate service cache/request-cache
ng generate service cache/caching-interceptor


Now change the content of the src/app/cache/request-cache.service.ts file to mirror the code below.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse } from '@angular/common/http';

const maxAge = 30000;
@Injectable({
  providedIn: 'root'
})
export class RequestCache  {

  cache = new Map();

  get(req: HttpRequest<any>): HttpResponse<any> | undefined {
    const url = req.urlWithParams;
    const cached = this.cache.get(url);

    if (!cached) return undefined;

    const isExpired = cached.lastRead < (Date.now() - maxAge);
    const expired = isExpired ? 'expired ' : '';
    return cached.response;
  }

  put(req: HttpRequest<any>, response: HttpResponse<any>): void {
    const url = req.urlWithParams;
    const entry = { url, response, lastRead: Date.now() };
    this.cache.set(url, entry);

    const expired = Date.now() - maxAge;
    this.cache.forEach(expiredEntry => {
      if (expiredEntry.lastRead < expired) {
        this.cache.delete(expiredEntry.url);
      }
    });
  }
}


The RequestCache service acts as a cache in memory. The put and get methods will store and retrieve HttpResponses based on the request data. Now replace the contents of src/app/cache/caching-interceptor.service.ts with the following.

import { Injectable } from '@angular/core';
import { HttpEvent, HttpRequest, HttpResponse, HttpInterceptor, HttpHandler } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestCache } from './request-cache.service';

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const cachedResponse = this.cache.get(req);
    return cachedResponse ? of(cachedResponse) : this.sendRequest(req, next, this.cache);
  }

  sendRequest(req: HttpRequest<any>, next: HttpHandler,
    cache: RequestCache): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          cache.put(req, event);
        }
      })
    );
  }
}


The CachingInterceptor can intercept any HttpRequest. It uses the RequestCache service to look for already stored data and returns it if possible. To set up the interceptor, open src/app/app.module.ts and add the following imports.

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RequestCache } from './cache/request-cache.service';
import { CachingInterceptor } from './cache/caching-interceptor.service';


Update the providers section to include the services

providers: [{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }],


With these changes, the application will cache the most recent requests and their responses. This means that you can navigate back from the details page and still see the search results in offline mode.

NOTE: In this version, I am keeping the cache in memory and not persisting it in the browsers localStorage. This means that you will lose the search results when you force a reload on the application. If you wanted to store the responses persistently, you would have to modify the RequestCache accordingly.

Remember not to use the ng serve command to test your PWA. Always build your project first, then start the server with:

ng build --prod --source-map
http-server-spa dist/AngularBooksPWA/ index.html 8080


Monitor Your Network’s Status

Open src/app/app.component.ts and add the following property and method.

offline: boolean;

onNetworkStatusChange() {
  this.offline = !navigator.onLine;
  console.log('offline ' + this.offline);
}


Then edit the ngOnInit method and add the following lines.

window.addEventListener('online',  this.onNetworkStatusChange.bind(this));
window.addEventListener('offline', this.onNetworkStatusChange.bind(this));


Finally, add a notice to the top-bar in src/app/app.component.html, before the div containing the search form.

<div *ngIf="offline">offline</div>


The application will now show an offline message in the top bar when the network is not available.

Pretty cool, don’t you think?!

Learn More About PWAs and Angular

In this tutorial, I’ve shown you how to create a progressive web app using Angular 7. Due to the effort put in by the Angular developers, it is easier than ever to achieve a perfect score for your PWA. With just a single command all the necessary resources and infrastructure is put into place to make your app offline-ready. In order to create a really outstanding PWA, there are many more improvements you can apply to your application. I have shown you how to implement a cache for HTTP requests as well as an indicator telling the users when they are offline.

The complete code for this project can be found at https://github.com/oktadeveloper/okta-angular-pwa-example.

In order to further improve the application, you might consider the following. You could add placeholder images for the book covers when the user is in offline mode. You should also make sure that delayed loading of images does not make the page jump around. Because PWAs are mainly useful for mobile devices, it is important to check that you can always scroll form inputs into view, even if the on-screen keyboard is open.

Learn More

Angular 7 (formerly Angular 2) - The Complete Guide

Learn and Understand AngularJS

Angular Crash Course for Busy Developers

The Complete Angular Course: Beginner to Advanced

Angular (Angular 2+) & NodeJS - The MEAN Stack Guide

Become a JavaScript developer - Learn (React, Node,Angular)

Angular (Full App) with Angular Material, Angularfire & NgRx

The Web Developer Bootcamp

ECommerce Mobile App Development | Ecommerce Mobile App Development Services

We are leading ecommerce mobile application development company. Hire our ecommerce mobile app developer for your custom Ecommerce project at competitive rates. **Know about [Top ECommerce Mobile App Development...

We are leading ecommerce mobile application development company. Hire our ecommerce mobile app developer for your custom Ecommerce project at competitive rates.

Know about Top ECommerce Mobile App Development Company

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.

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!