ng add PWA

In this lesson, we’ll use the Angular CLI to create all the things we need to transform our application into a PWA.

For that, open your terminal, navigate to the root of your project and type:

ng add @angular/pwa

Here is what that command does:

  • It generates icons in different sizes, and those will be used for the add to home screen icon of your app, it’s the default angular badge, so change them for your brand.
  • It creates a manifest.webmanifest file and links it in your main index.html file, and adds it to the build process in angular.json. (We’ll explain that file below).
  • It installs the @angular/service-worker package.
  • It imports the ServiceWorkerModule in your app.module.ts file and enables it when you run production builds. (NOTE: It also imports the environment.ts file, so if you already had it there for something you’ll get a double import, make sure you remove it.)
  • Adds the ngsw-config.json file which is the configuration for our service worker.

This lesson is part of a more detailed PWA course, you can find the complete course at https://pwa.jsmobiledev.com/.

If you try to test this right now with ng serve your app will work as if you didn’t do any of this, this is because service workers are only enabled on production builds.

So if you want to test your PWA locally, you might want to install the node http-server package:

npm install -g http-server

Then, you’d want to run a production build, and let the http-server serve you that build:

ng build --prod
http-server -p 8080 -c-1 path_to_build_folder/

Where path_to_build_folder/ is the production build your app just created, it’s usually www or dist/something/.

That one will launch a server listening on port 8080, so going to localhost:8080 will take you to your app.

Now, let’s work through everything that was created to make sure we understand what’s going on (also, because we can’t just trust the default :-P).

Icons

The command generates icons for all needed sizes, but it’s the angular default badge, you’ll find them under src/assets/icons.

They’ll all have their names like icon-72x72.png where the numbers are the size in pixels, and all of them will be linked in your manifest already.

Here you should take a moment to replace them with your own. There are several tools online that create them all based on a high-resolution version of it, also, there are packages that can handle it.

Web Manifest File

By default, the CLI created a file called manifest.webmanifest in the src folder.

If you open it, you’ll see something similar to this:

{
  "name": "app",
  "short_name": "app",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ]
}

Let’s go through those properties and see what they do:

  • name: This is the name of your application.
  • short_name: It’s the short name of your app, this is what you see under the app’s icon on the home screen.
  • theme_color: From what I’ve seen this is handled differently in different devices/OS/browsers, for example, on Android’s task switcher, the theme color surrounds the site.
  • background_color: This is a placeholder background color while your styles load, it’s also used as a bg color for the Splash Screen.
  • display: Controls how are users see the app, it has several options:
    • fullscreen takes all of the available space.
    • standalone is similar to fullscreen. It won’t have navigation or URL bar, but it will show the status bar.
    • minimal-ui Similar to standalone but the browser might choose to have UI for navigation.
    • browser is a regular browser window.
  • scope: Defines the navigation scope of the web app context. It restricts to which pages can the manifest be applied. For a full PWA we generally go with / but let’s say a subset of your site is the PWA, you can do something like scope: '/app/' and the manifest will be applied only to the app subdirectory.
  • start_url: This is our start URL, what will be loaded once we click on the home screen button.
  • icons: These are the links to the icons we generated.

You can now change those defaults into something that makes more sense for your application 😃

By the way, this file is auto-linked in the index.html file and added into the angular.json build process to make sure our build process includes it.

Service Worker

Our application already did three things regarding service workers:

  • It installed the @angular/service-worker package.
  • Imported the service worker on our module file.
  • Generated a configuration file for our SW, since it will be autogenerated on build time based on that configuration.

The configuration file is located at the root of our project, and it’s called ngsw-config.json, open it, you’ll find something like this:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": ["/assets/**", "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"]
      }
    }
  ]
}

This is where you configure everything service worker related. Inside the assetGroups properties we’re telling our SW how we want our cache to work.

For example, one creating one group called app, this is the essentials we need to get out app working, our HTML, CSS, and JS. When we say that the install mode is prefetch, the service worker is going to fetch and download all of our HTML, CSS, JS file as soon as you load the page.

On the other hand, the group named assets isn’t essential, it’s for images, fonts, and styles, so what we’re doing is that after the user loads the first page, the SW is going to start downloading those as it needs them, instead of fetching them all at once.

You can also create your groups. Let’s say you want to cache a URL response, let’s say you have some data on an API that you know it’s static and won’t change that often, you can create a group that caches the response from that URL:

{
  "name": "offline",
  "installMode": "lazy",
  "resources": {
    "urls": ["https://jsmobiledev.com/countryList.json"]
  }
}

Now the first time I call "https://jsmobiledev.com/countryList.json" the service worker is going to cache that response to have available for us.

Good Practices

Right now, you have your PWA ready, and it will work with all the defaults.

But we need to make sure we change some of those to follow some best practices to improve our users' journeys.

The first one is the update. You need to make sure your users are always on the most updated version of the app. That’s not complicated since it’s a web app; on every refresh, your users will have the most updated version.

What you can do is create a listener that listens for updates in the background and lets your app know when it needs to refresh.

For that, we’ll use angular SW package. Go into your app.component.ts file and first, import the package and inject it into the constructor:

import { Component } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  constructor(private swUpdate: SwUpdate) {}
}

Now, let’s create an initialization function that checks if there’s an update available and subscribes to it:

import { Component } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  constructor(private swUpdate: SwUpdate) {
    this.initializeApp();
  }

  initializeApp(): void {
    if (this.swUpdate.available) {
      this.swUpdate.available.subscribe(() => {
        if (confirm('A new version is available. Load it?'))
          window.location.reload();
      });
    }
  }
}

We’re checking if there’s an update available, and subscribing to it when the update is available, we’ll trigger an alert to reload the page.

Installation

The other good practice I wanted to talk to you about was Installation. Right now, your app will prompt the “Add to home screen” banner. This is automatic, and it’s handled by the browser.

If you want to leave it at that, fine, it’s your choice, and it works, but consider that not everyone likes to be prompted to install to the home screen as soon as they load your page.

You can actually intercept the browser and stop it from displaying that prompt, and then present it yourself at a later time, maybe after the user has spent some time navigating through your site 😃

The browser triggers an event called beforeinstallprompt, you can add a listener to it like this:

window.addEventListener('beforeinstallprompt', e => {});

Now we need to prevent the default behavior, in other words, we need to stop it from showing the automatic prompt.

window.addEventListener('beforeinstallprompt', e => {
  e.preventDefault();
});

And now, we want to store the permission somewhere else, like in service or something:

window.addEventListener('beforeinstallprompt', e => {
  e.preventDefault();
  this.myCoolService.customInstallPrompt = e;
});

That way you can later bind the prompt to anything you want, for example, let’s say you have a custom button that is called “Install ME”.

The click handler to that button should call:

this.myCoolService.customInstallPrompt.prompt();

Which will trigger the banner to add to the home screen.

Like I said before, you can let the browser take care of this, but with a little extra effort you can handle it yourself and provide a better experience for your users 😃.

And that’s it, remember that I’m always one email away if you have any questions, in the next lesson, we’ll add push notifications from our app. 🚀