Welcome to the free & complete Angular Tutorial.

In this blog post, we are going to quickly learn about how to create PWA (Progressive Web App) in Angular and deploy it to Firebase.

Come and join me to develop an application dedicated for Dad Jokes. 🙂 Though the post is quite long, you will create an amazing app at the end of the post.

I split this post into three sections as follows:

  1. What is PWA and it’s advantages over traditional web app?
  2. PWA App Development:
    1. Create new Angular project
    2. Create web components (Pages)
    3. Make web app PWA enabled
    4. Configure Joke API
  3. Deploy code in Firebase

Ok, let’s begin.

Section 1: What is PWA and it’s advantages over traditional web app?

According to Mozilla Web Docs:

Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications. These apps work everywhere and provide several features that give them the same user experience advantages as native apps.

Mozilla – MDN Web Docs
  • Progressive Web Apps (PWA) is a bunch of guidelines to make use of Modern web browser features to develop Web application that look more like a Native app.
  • It uses Service Worker (Browser feature) to interact with backend to seamlessly refresh content on UI.
  • User will be notified to install the Web app as an App in Mobile device, so no need to visit App store to download and install apps.
  • PWA works offline as well unlike traditional web apps. It talks to backend once internet is ON similar to Native apps.

PWA Advantages:

  • Installable:
    • PWA apps get installed like Native app with App icon on Home Screen, so it’s easy for users to access the App from Home.
  • Linkable:
    • Link to any part of the PWA with no complex configuration needed as in Native apps.
    • Modify and deploy changes instantly and notify Users about the change. Again, no complex process involved in updating Apps.
  • Progressive:
    • App works for every user that use browsers that support PWA service workers and other modern web features.
  • Responsive:
    • App works great across all types of devices (Mobile, Tablet, Laptop etc.,), thanks to Media queries.
  • Network independent:
    • When no network available, app should still continue to work (Background operation like fetching data from remote server etc., is resumed only when network is back online).
  • More advantages can be found here.

Section 2: PWA App Development

For creating “Dad Joke” PWA app, we need to start creating simple Angular project. Go ahead and follow below steps to create angular project, design screens, enable PWA, and configure Joke API.

Excited?

Step 2.1: Create new Angular project:

Hope you already have Node JS and Angular CLI installed, otherwise please check here.

Run the following command to create new Angular project.

ng new DadJoke

Run the following command to add Angular material to the project. While installing Angular Material, we need to choose prebuilt Angular theme. I chose indigo-pink theme.

cd DadJoke
ng add @angular/material

Just in case if the option to choose prebuilt theme was not prompted, add following code in Styles.css as a first line of code (without editing any other CSS code).

@import "@angular/material/prebuilt-themes/indigo-pink.css";

The above ng add command will additionally perform the following configurations for us:

  • Add project dependencies to package.json
  • Add the Roboto font to your index.html
  • Add the Material Design icon font to your index.html
  • Add a few global CSS styles to:
    • Remove margins from body
    • Set height: 100% on html and body
    • Set Roboto as the default application font

You successfully added Angular Material theme.

Step 2.2: Create web components (Pages)

We are gonna have three components:

  • App Component (Default main component created by ng new command)
  • Home Component (To display joke)
  • About Component (To display App detail)

Step 2.2.1: Create Home Component

Run the following Angular navigation semantic command to create “Home component” and also this adds Side Nav bar to it. Learn more about Angular Material Schematics.

 ng generate @angular/material:navigation home

Replace app.component.html code with home.component.html. The idea is here to have side navigation bar in app component rather than home component.

app.component.html:

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    fixedInViewport
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
    [opened]="(isHandset$ | async) === false"
  >
    <mat-toolbar>Menu</mat-toolbar>
    <mat-nav-list>
      <a mat-list-item href="#">Link 1</a>
      <a mat-list-item href="#">Link 2</a>
      <a mat-list-item href="#">Link 3</a>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>DadJoke</span>
    </mat-toolbar>
    <!-- Add Content Here -->
  </mat-sidenav-content>
</mat-sidenav-container>

Similarly replace app.component.css with home.component.css code; Do the same for Typescript as well, make sure you copied import statements and constructors as well..

app.component.css

.sidenav-container {
  height: 100%;
}

.sidenav {
  width: 200px;
}

.sidenav .mat-toolbar {
  background: inherit;
}

.mat-toolbar.mat-primary {
  position: sticky;
  top: 0;
  z-index: 1;
}

app.component.ts

import { Component } from "@angular/core";
import { BreakpointObserver, Breakpoints } from "@angular/cdk/layout";
import { Observable } from "rxjs";
import { map, shareReplay } from "rxjs/operators";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  isHandset$: Observable<boolean> = this.breakpointObserver
    .observe(Breakpoints.Handset)
    .pipe(
      map(result => result.matches),
      shareReplay()
    );
  constructor(private breakpointObserver: BreakpointObserver) {}
}

Step 2.2.2: Massage Home Component:

You successfully copied Navigation code generated by Angular Material Schematic from Home component to App component. Now we need to add UI and Typescript code to Home component.

Add below CSS to Styles.css (Make sure you just append it with existing CSS code).

Styles.css

.main-div {
  height: 75%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.card-content {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50%;
  width: 75%;
  font-size: 1.75em;
  line-height: 1.75em;
}

Before adding below Html code to this component, import Mat-Card component module in app.module.ts and add it to imports Array in NgModule decorator, as we will use Mat-Card component in HTML.

import { MatCardModule } from "@angular/material/card";

home.component.html

<div class="main-div">
  <mat-card class="card-content">{{ jokeStr }}</mat-card>
</div>

home.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-home",
  templateUrl: "./home.component.html",
  styleUrls: ["./home.component.css"]
})
export class HomeComponent {
  jokeStr: string;
}

Step 2.2.3: Create About Component:

Run following command to create About Component, so this app is going to have three components – App, Home and About.

ng g c about

about.component.html

<div class="main-div">  
   <mat-card class="card-content">
     Dad Joke app - To demonstrate the concept of Progressive Web App.
   </mat-card>
</div>

about.component.css

.main-div {
    height: 75%;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .card-content {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 50%;
    width: 75%;
    font-size: 1.75em;
    line-height: 1.75em;
  }

Step 2.2.3: Configure Routing:

OK, one last thing. We need to configure Routes for Home and About components in app.module.ts and add Router links and Router-outlet in app.component.html.

Import Routing module and Routes, create appRoutes object and add it to forRoot method as shown in the highlighted lines below:

app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

import { AppComponent } from "./app.component";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { LayoutModule } from "@angular/cdk/layout";
import { MatToolbarModule } from "@angular/material/toolbar";
import { MatButtonModule } from "@angular/material/button";
import { MatSidenavModule } from "@angular/material/sidenav";
import { MatIconModule } from "@angular/material/icon";
import { MatListModule } from "@angular/material/list";
import { HomeComponent } from "./home/home.component";
import { AboutComponent } from "./about/about.component";

import { MatCardModule } from "@angular/material/card";

const appRoutes: Routes = [
  { path: "", component: HomeComponent },
  { path: "about", component: AboutComponent }
];

@NgModule({
  declarations: [AppComponent, HomeComponent, AboutComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    LayoutModule,
    MatToolbarModule,
    MatButtonModule,
    MatSidenavModule,
    MatIconModule,
    MatListModule,
    MatCardModule,
    RouterModule.forRoot(appRoutes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

app.component.html

Update routeLinks and Router-Outlet as in the below highlighted lines.

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    fixedInViewport
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
    [opened]="(isHandset$ | async) === false"
  >
    <mat-toolbar>DJ</mat-toolbar>
    <mat-nav-list>
      <a mat-list-item  routerLink="">Home</a>
      <a mat-list-item  routerLink="about">About</a>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>Dad Joke of the day..</span>
    </mat-toolbar>
    <!-- Add Content Here -->
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

Run the app locally using following command:

ng s -o

Step 2.3: Make web app PWA enabled

It’s time to enable PWA functionalities in the app we designed.

Service worker is the web api feature that enables the Web app to work like Native app by caching the content, talk to server seamlessly in the background, refreshes the cache when new App is deployed and more.

Running below command adds service worker and other boiler plate code to our Angular project.

ng add @angular/pwa

The above command completes the following actions:

  1. Adds the @angular/service-worker package to your project.
  2. Enables service worker build support in the CLI.
  3. Imports and registers the service worker in the app module.
  4. Updates the index.html file:
    • Includes a link to add the manifest.json file.
    • Adds meta tags for theme-color.
  5. Installs icon files to support the installed Progressive Web App (PWA).
  6. Creates the service worker configuration file called ngsw-config.json, which specifies the caching behaviors and other settings.

We just skipped writing a lot of lines of code to handle PWA, as this angular schematic just took care of it. Thanks to Angular Schematics. 🙂

Run npm i quickly to install the dependencies added in package.json (Sometimes npm goes cranky, it doesn’t install dependencies while running ng add – schematic command).

Build app using following command:

ng build --prod

Run the app we just built using lite-server. We need to run it using server in order to enable PWA behavior in the app (like prompting Install button in browser etc.,), ng serve command will not help in this case.

lite server dist/dadjoke

All ready! Woohoo we successfully added PWA package to Angular app and it’s working great.

Step 2.4: Configure Joke Api

Let’s wire Joke API to display joke on the App. Am going to use https://icanhazdadjoke.com/api which is free and works great.

Joke Service:

Create new service called Joke, which makes a call to Joke API to fetch random Joke.

ng g s joke

joke.service.ts

Add below code to the service you just created.

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

@Injectable({
  providedIn: "root"
})
export class JokeService {
  isInit: boolean = true;
  jokeStr: string =
    "What are computers favorite snacks? Microchips, phish sticks, and cookies. But just a few bytes of each.";

  constructor(private http: HttpClient) {}

  getRandomjoke() {
    return this.http.get("https://icanhazdadjoke.com/slack");
  }
}

Add following code to invoke Joke service method to fetch joke and display on UI.

home.component.ts

import { Component } from "@angular/core";
import { JokeService } from "../joke.service";

@Component({
  selector: "app-home",
  templateUrl: "./home.component.html",
  styleUrls: ["./home.component.css"]
})
export class HomeComponent {
  jokeStr: string;

  constructor(private jokeSvc: JokeService) {}

  ngOnInit() {
    if (this.jokeSvc.isInit) {
      this.getJoke();
      this.jokeSvc.isInit = !this.jokeSvc.isInit;
    } else {
      this.jokeStr = this.jokeSvc.jokeStr;
    }
  }

  getJoke() {
    this.jokeSvc
      .getRandomjoke()
      .subscribe(
        data =>
          (this.jokeSvc.jokeStr = this.jokeStr = data["attachments"][0].text)
      );
  }
}

Run the app now using following code:

ng s -o

Oh ho. I got below error 🙁 I forgot to include HttpClientModule in App Module.

Just sharing this info, you may find it useful from “Fixing the run time error” perspective.

I just fixed the error by importing HttpClientModule and adding it to imports array in app.module.ts

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

imports:[
.
.
HttpClientModule
.
.
]

Section 3: Deploy code to Firebase

Login to Firebase with your Google account

Click on “Add project”, to create new Project.

Enter the name of you project [Click next], Enable google analytics (optional) [Click continue / Create Project].

Goto “Hosting” and click “Get started” button to for further instructions. If you already have Firebase installed, please skip this step.

Install Firebase using following npm command:

npm install -g firebase-tools

Open terminal/Command prompt and navigate to theAngular project. Run following commands to login, it takes you to browser for login.

firebase login

Initialize Firebase setup in Angular project by running below command.

firebase init

It prompts for following questions, choose answers carefully..

  • Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites
  • Please select an option: Use an existing project
  • Select a default Firebase project for this directory: dad-joke-pwa (Dad Joke PWA)
  • What do you want to use as your public directory? dist/dadjoke
  • Configure as a single-page app (rewrite all urls to /index.html)? Yes
  • File dist/dadjoke/index.html already exists. Overwrite? No

One last time, build Angular app in production mode.

ng build --prod

All set, run following command to deploy code to Firebase. It deploys code from dist/dadjoke to remote server, as we chose dist/dadjoke as the public folder during Firebase project setup above.

firebase deploy 

Demo:

Check out the demo from this URL – https://dad-joke-pwa.web.app/

Desktop:

Mobile:

If you find this post useful, please feel free to share it with your friends / colleagues.

Happy learning!