I utilized the guidelines presented in this article to implement Progressive Web App (PWA) features within my Angular application. The primary objective was to establish the capability to detect the availability of new application versions. This would prompt the client-side interface to display a confirmation modal to users, notifying them of the updated application version. The modal message would read as follows: "New version available. Load New Version?" Subsequently, users can proceed to reload the application, ensuring the seamless integration of the latest version.
Code is as below:
ngsw-config.json:
{
"$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)"
]
}
}
]
}
app.module.ts:
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { BatchAutocompleteLibModule } from '@**service/batch-autocomplete-lib';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import {ToastrModule} from 'ngx-toastr';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LaravelRequestInterceptor } from './common/laravel-request.interceptor';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommonService } from './common/common.service';
import { UserService } from './services/auth/user.service';
import { AuthSetupComponent } from './common/components/auth-setup/auth-setup.component';
import { LogoutComponent } from './common/components/logout/logout.component';
import { GoogleTagManagerModule } from 'angular-google-tag-manager';
import {environment} from '@environments/environment';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {AlertModule} from '@app/common/components/alert';
import { StoreModule } from '@ngrx/store';
import { navBarReducer } from './store/navigation/navBar.reducer';
import {
NgxAwesomePopupModule,
DialogConfigModule,
ConfirmBoxConfigModule,
ToastNotificationConfigModule
} from '@costlydeveloper/ngx-awesome-popup';
import { NotFoundComponent } from './common/components/not-found/not-found.component';
import { ServiceWorkerModule } from '@angular/service-worker';
@NgModule({
declarations: [
AppComponent,
AuthSetupComponent,
LogoutComponent,
NotFoundComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
NgbModule,
BrowserModule,
HttpClientModule,
BatchAutocompleteLibModule.forRoot(environment.autocompleteLibUrl, environment.autocompleteLibToken),
GoogleTagManagerModule.forRoot({
id: environment.GTM_id,
...environment.GTM_params
}),
ToastrModule.forRoot(),
AlertModule,
NgxAwesomePopupModule.forRoot({
colorList: {
success: '#67E583',
info: '#3683BC',
warning: '#E7DD00',
danger: '#F7775B',
primary: '#3683BC',
secondary: '#e46464'
}
}), // Essential, mandatory main module.
DialogConfigModule.forRoot(), // Needed for instantiating dynamic components.
ConfirmBoxConfigModule.forRoot(), // Needed for instantiating confirm boxes.
ToastNotificationConfigModule.forRoot(), // Needed for instantiating toast notifications.
StoreModule.forRoot({ navBar: navBarReducer }), ServiceWorkerModule.register('ngsw-worker.js', {
enabled: true
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first).
// registrationStrategy: 'registerWhenStable:30000'
}),
],
providers: [
CommonService,
UserService,
{provide: HTTP_INTERCEPTORS, useClass: LaravelRequestInterceptor, multi: true},
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
bootstrap: [AppComponent]
})
export class AppModule { }
angular.json:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": "53ed1256-56ee-4746-9a54-3e6bd73c6b2d"
},
"version": 1,
"newProjectRoot": "projects",
"projects": {
"***-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/***-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
"src/.htaccess",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss",
"node_modules/datatables.net-dt/css/jquery.dataTables.min.css",
"node_modules/ngx-toastr/toastr.css",
"node_modules/ng2-daterangepicker/assets/daterangepicker.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/popper.js/dist/umd/popper.js",
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
"node_modules/apexcharts/dist/apexcharts.min.js",
"node_modules/datatables.net/js/jquery.dataTables.min.js"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "20kb"
}
]
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"development": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"tfdev": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.tfdev.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"local": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "***-angular:build"
},
"configurations": {
"production": {
"browserTarget": "***-angular:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "***-angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss",
"src/assets/css/angular-material-ui.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "***-angular:serve"
},
"configurations": {
"production": {
"devServerTarget": "***-angular:serve:production"
}
}
}
}
}
},
"defaultProject": "***-angular"
}
package.json:
{
"name": "***-angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular-slider/ngx-slider": "^2.0.4",
"@angular/animations": "^12.2.16",
"@angular/common": "~12.2.7",
"@angular/compiler": "~12.2.7",
"@angular/core": "~12.2.7",
"@angular/forms": "~12.2.7",
"@angular/localize": "~12.2.7",
"@angular/platform-browser": "~12.2.7",
"@angular/platform-browser-dynamic": "~12.2.7",
"@angular/router": "~12.2.7",
"@angular/service-worker": "~12.2.7",
"@batch***/batch-autocomplete-lib": "0.0.28",
"@costlydeveloper/ngx-awesome-popup": "^3.1.3",
"@ng-bootstrap/ng-bootstrap": "^6.2.0",
"@ngrx/store": "^12.5.1",
"@stripe/stripe-js": "^1.29.0",
"@tailwindcss/forms": "^0.3.3",
"@tailwindcss/typography": "^0.4.1",
"angular-datatables": "^13.0.1",
"angular-google-tag-manager": "1.4.4",
"angular-mydatepicker": "^0.11.5",
"apexcharts": "^3.28.3",
"bootstrap": "^4.4.0",
"datatables.net": "^1.11.3",
"datatables.net-dt": "^1.11.3",
"flatpickr": "^4.6.13",
"jquery": "^3.6.0",
"lodash-es": "^4.17.21",
"moment": "^2.29.3",
"moment-timezone": "^0.5.40",
"ng-apexcharts": "^1.5.12",
"ng2-daterangepicker": "^3.0.1",
"ngx-daterangepicker-material": "^5.0.1",
"ngx-mask": "^13.1.12",
"ngx-pagination": "^6.0.2",
"ngx-spinner": "^11.0.2",
"ngx-stripe": "^13.1.0",
"ngx-toastr": "^14.3.0",
"popper.js": "^1.16.1",
"pusher-js": "^7.1.1-beta",
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~12.2.7",
"@angular/cli": "~12.2.7",
"@angular/compiler-cli": "~12.2.7",
"@angular/language-service": "~12.2.7",
"@types/datatables.net": "^1.10.21",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/jquery": "^3.5.9",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"tailwindcss": "^2.2.16",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.3.5"
}
}
src/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>****</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
</head>
<!--This is for production enviroment. Make sure this during merging the code-->
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>
src/manifest.webmanifest:
{
"name": "***-angular",
"short_name": "***-angular",
"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"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}
src/app/app.component.ts:
ngAfterViewInit(): void {
setTimeout(() => {
if (this.swUpdate.isEnabled) {
setInterval(() => {
this.swUpdate.checkForUpdate();
}, 3000)
this.swUpdate.available.subscribe(() => {
if(confirm("New version available. Load New Version?")) {
window.location.reload();
}
});
}
}, 1000)
}
When a user first arrives on the page, there's a short pause of 1 second. During this time, we check if the service worker is active. This delay is needed to ensure that the subsequent code works properly when we look at the if (this.swUpdate.isEnabled)
part.
To make things better, I want to utilize Angular's built-in versionUpdates
feature. This would eliminate the need for the 1-second delay and the manual checking that happens every 3 seconds using setInterval
. With versionUpdates
, the process of detecting updates in the service worker file becomes more straightforward and efficient, making the system work better overall.
The objective is to eliminate the reliance on both the setTimeout
and setInterval
functions. Despite researching extensively, I haven't found a suitable solution. I'm seeking assistance from anyone who can review the current implementation and provide guidance on whether it's possible to achieve this goal, along with potential approaches. Your insights and assistance in this matter would be greatly appreciated.