Angular PWA guide for developers Part-2

Syed Khizaruddin
Frontend Weekly
Published in
7 min readJun 17, 2024

Progressive web apps

progressive web app- guide for developers part 2
Photo by rivage on Unsplash

This part is the continuation of part 1 you can check it here.

Application Shell:

Whenever a web page is loaded on the browser if we open performance tab from chrome dev tools and capture how much time it took to load all html css, js and bundles scripts etc., we will come to know that for initial 300 milliseconds to 400 milliseconds (may vary from application to application) there was a blank page no content nothing was seen, there should be at least a loading behavior to give user some feedback that the application is getting loaded, this 300 to 400ms or it may varies from application to application, only an empty index.html is served and as we can see in index.html we don’t have any content just app-root and loading… is there so it takes time to initialize angular application and load into browser,

To increase the performance we need to show some html content before application is loaded, like a placeholder or some menu background or anything, this HTML CSS content which is loaded before angular kicks in is called as Application shell.

In order to add some html css or js before the angular application loads we need to use angular universal

To create an appshell you just need to run below command,

ng generate app-shell --client-project my-app --universal-project server-app

Note: Make sure to replace my-app with your project name

You’ll notice that this command generated a new component called the AppShellComponent.

This is the component that is used as an app shell so any pieces of your app shell like navigation bar loader goes in this component, which will be seen at the time of loading application.

We can see there is server configurations in angular.json, and new files were created which are app.server.module, main.server.ts, tsconfig.server.json, some new packages were installed like @angular/platform-server,

Changes in angular.json after running above command.

  "app-shell": {
"builder": "@angular-devkit/build-angular:app-shell",
"options": {
"browserTarget": "angular-pwa-course:build",
"serverTarget": "angular-pwa-course:server",
"route": "shell"
},
"configurations": {
"production": {
"browserTarget": "angular-pwa-course:build:production",
"serverTarget": "angular-pwa-course:server:production"
}
}
}

Lets understand route inside app-shell, for understanding it we need to see the changes inside app.server.module.ts file

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { Routes, RouterModule } from '@angular/router';
import { AppShellComponent } from './app-shell/app-shell.component';

const routes: Routes = [ { path: 'shell', component: AppShellComponent }];

@NgModule({
imports: [
AppModule,
ServerModule,
RouterModule.forRoot(routes),
],
bootstrap: [AppComponent],
declarations: [AppShellComponent],
})
export class AppServerModule {}

you can see there are some changes in here for server module there is routing configurations, these routing configs are different than what it is for client routing configurations,

When we have a navigation event to ‘shell’ as defined inside routes constant above, we should replace the app components router-outlet with app-shell component.

Let’s change app-shell component,

import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-shell',
template: `
<img class="loading-indicator" src="/assets/loading.gif" alt="loading gif" />
`,
styles: [
`.loading-indicator {
height: 300px;
margin: 0 auto;
}`
]
})
export class AppShellComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

After successful configuration let’s see app-shell in action,

Run this command,

ng run my-app:app-shell

Stop all service worker by unregistering it, then start the application using http-server and you will be able to see app-shell for a split second.

Note- For Angular 18 you can check on its official docs page, it’s straight forward my goal is to make you understand how app-shell works, so that you can configure it in any angular version.

web notification- Angular pwa part 2
Photo by Brett Jordan on Unsplash

Web Notifications:

For setting up, please use this repository here, and clone the 1-notifications branch, install dependencies using npm install,

After that run the backend server using npm run server and in another terminal use npm run start:prod to build our application and serve the same using http server,

If the server doesn’t runs, change the content of server.ts to this below,


// launch an HTTP Server
const httpServer = app.listen(9000, () => {
console.log("HTTP Server running at http://localhost:" + 9000);
});

We need to change the ngsw-config.json as below,

{
"index": "/index.html",
"assetGroups": [{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest", // changes from here removed versionedFiles
"/*.css", // changes
"/*.js" // changes
]
}
}, {
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
}],
"dataGroups": [
{
"name": "lessons-api",
"urls": [
"/api/lessons"
],
"cacheConfig": {
"strategy": "freshness",
"timeout":"10s",
"maxAge": "1d",
"maxSize": 100
}
}
]
}

VersionFiles are not supported onwards hence we will use files inside resources.

We need to add a two button one for subscribe and another send button so subscribe button will ask for user permission for allowing notification, send will send news letter.

lessons.component.html

<div class="wrapper">
<button class="subscribe-btn" (click)="onSubscribe()">
Subscribe
</button>
<button class="send-btn" (click)="onMessageSend()">
Send message
</button>
</div>

lessons.component.ts

import {Component, OnInit} from '@angular/core';
import {LessonsService} from "../services/lessons.service";
import {Observable, of} from 'rxjs';
import {Lesson} from "../model/lesson";
import {SwPush} from "@angular/service-worker";
import {NewsletterService} from "../services/newsletter.service";
import {catchError} from 'rxjs/operators';

@Component({
selector: 'lessons',
templateUrl: './lessons.component.html',
styleUrls: ['./lessons.component.css']
})
export class LessonsComponent implements OnInit {

lessons$: Observable<Lesson[]>;
isLoggedIn$: Observable<boolean>;

sub: PushSubscription;
readonly VAPID_PUBLIC_KEY = "BLnVk1MBGFBW4UxL44fuoM2xxQ4o9CuxocVzKn9UVmnXZEyPCTEFjI4sALMB8qN5ee67yZ6MeQWjd5iyS8lINAg";
constructor(
private lessonsService: LessonsService,
private swPush: SwPush,
private newsletterService: NewsletterService
) {}

ngOnInit() {
this.loadLessons();
}

loadLessons() {
this.lessons$ = this.lessonsService.loadAllLessons().pipe(catchError(err => of([])));
}

subscribeToNotifications() {
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
})
.then(sub => {
this.sub = sub;
console.log("Notification Subscription: ", sub);
this.newsletterService.addPushSubscriber(sub).subscribe(
() => console.log('Sent push subscription object to server.'),
err => console.log('Could not send subscription object to server, reason: ', err)
);
})
.catch(err => console.error("Could not subscribe to notifications", err));
}
sendNewsletter() {
console.log("Sending Newsletter to all Subscribers ...");
this.newsletterService.send().subscribe();
}
}

In order to get serverPublicKey we would be needing web push library, run below command,

npm i -g web-push 

After installing we need to generate vapid keys using below command,

web-push generate-vapid-keys --json

This will give us the keys

Using this unique keys the web browser, is able to identify our server and will be able to handle notifications.

Difference between public key and private key:

The public key is a publicly available identifier of a given application server that can send push notifications, these information is public and is available in user browser, this key is needed to request user to show notifications, it can be publicly available and can be added in the client code.

The private key is going to be used to generate push notifications and send them to the users, only those who have private key in possession will be able to generate notifications and send them to the users, private keys cannot be present on the client code, and it should be kept private at a server level.

How push notification works?

When user clicks on subscribe button the below content is run,

subscribeToNotifications() {
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
})
.then(sub => {
this.sub = sub;
console.log("Notification Subscription: ", sub);
this.newsletterService.addPushSubscriber(sub).subscribe(
() => console.log('Sent push subscription object to server.'),
err => console.log('Could not send subscription object to server, reason: ', err)
);
})
.catch(err => console.error("Could not subscribe to notifications", err));
}

On checking the console log, after user has allowed to show notification confirmation dialog, we can see there is an subscription object as given below, which shows some details about push notification subscription, we can see there is an endpoint which points to fcm.googleapis.com, for this you need to understand that chrome is developed by google and all behavior related to chrome is handled by google so fcm here is firebase cloud messaging service, and in URL we can see there is unique instance and code which allows google to understand which browser instance, needs push notifications, so google itself takes command to send push notification to the user browser, and it is done because if some user gets so many notification google has rights to blacklist those websites which sends so many notifications.

Angular PWA part 2- push notifications

So, we need to send this object to our server, so that notification messages can be sent from our server to google fcm service, and from there fcm will send notification to users browser instance.

If user denied notification popup we can head to chrome browser settings
chrome://settings/content/notifications we can see list of all websites to which we have allowed notification or denied notifications.

Next we need to send these notifications from server to the fcm service which was stored when user allowed,

import {USER_SUBSCRIPTIONS} from "./in-memory-db";

const webpush = require('web-push');


export function sendNewsletter(req, res) {

console.log('Total subscriptions', USER_SUBSCRIPTIONS.length);

// sample notification payload
const notificationPayload = {
"notification": {
"title": "Angular News",
"body": "Newsletter Available!",
"icon": "assets/main-page-logo-small-hat.png",
"vibrate": [100, 50, 100],
"data": {
"dateOfArrival": Date.now(),
"primaryKey": 1
},
"actions": [{
"action": "explore",
"title": "Go to the site"
}]
}
};


Promise.all(USER_SUBSCRIPTIONS.map(sub => webpush.sendNotification(
sub, JSON.stringify(notificationPayload) )))
.then(() => res.status(200).json({message: 'Newsletter sent successfully.'}))
.catch(err => {
console.error("Error sending notification, reason: ", err);
res.sendStatus(500);
});

}

You can see the notification payload which we can configure while sending notification to fcm service, the USER_SUBSCRIPTIONS are all the users which are subscribed to the push notifications.

That’s it for now..

Stay tuned for more upcoming frontend related blogs.

TLDR;

Follow, for more programming blogs, linkedin here, instagram here.

And please checkout other blogs from here.

Buy me a coffee here.

--

--