We can access a web application using different types of devices, such as a desktop, mobile, tablet, and various network types, such as broadband, Wi-Fi, and cellular. A web application should work seamlessly and provide the same user experience independently of the device and the network of the user.
Progressive Web Apps (PWA) is a collection of web techniques for building web applications with previous considerations in mind. One popular technique is the service worker, which improves the loading time of a web application. In this chapter, we will use the service worker implementation of the Angular framework to build a PWA that displays the weather of a city using the OpenWeather API.
We will cover the following topics in detail:
Traditional web applications are usually hosted in a web server and are immediately available to any user at any given time. Native applications are installed on the device of the user, have access to its native resources, and can work seamlessly with any network. PWA applications stand between the two worlds of web and native applications and share characteristics from both. A PWA application is a web application that is based on the following pillars to convert into a native one:
Converting a web application into a PWA involves several steps and techniques. The most essential one is configuring a service worker. The service worker is a mechanism that is run on the web browser and acts as a proxy between the application and an external HTTP endpoint or other in-app resources such as JavaScript and CSS files. The main job of the service worker is to intercept requests to those resources and act on them by providing a cached or live response.
Important note
The service worker is persisted after the tab of the browser is closed.
The Angular framework provides an implementation for the service worker that we can use to convert our Angular applications into PWA.
It also contains a built-in HTTP client that we can use to communicate with a server over HTTP. The Angular HTTP client exposes an observable-based API with all standard HTTP methods such as POST and GET. Observables are based on the observer pattern, which is the core of reactive functional programming. In the observer pattern, multiple objects called observers can subscribe to an observable and get notified about any changes to its state. An observable dispatches changes to observers by emitting event streams asynchronously. The Angular framework uses a library called RxJS that contains various artifacts for working with observables. One of these artifacts is a set of functions called operators that can apply various actions on observables such as transformations and filtering. Next, let's get an overview of our project.
In this project, we will build a PWA application to display the weather conditions of a city. Initially, we will learn how to configure the OpenWeather API, which we will use to get weather data. We will then learn how to use the API to display weather information in an Angular component. We will see how to convert our Angular application into PWA using a service worker. We will also implement a notification mechanism for our application updates. Finally, we will deploy our PWA application into the Firebase hosting provider.
Build time: 90 minutes
The following software tools are required to complete this project:
The OpenWeather API has been created by the OpenWeather team and contains current and historical weather information from over 200,000 cities worldwide. It also supports forecast weather data for more detailed information. In this project, we will focus on the current weather data.
We need to get an API key first to start using the OpenWeather API:
You will see a list of all available APIs from the OpenWeather team.
You will be redirected to the page with the available pricing schemes of the service. Each scheme supports a different combination of API calls per minute and month. For this project, we are going to use the Free tier.
You will be redirected to the sign-up page of the service.
A confirmation message will be sent to the email address that you used to create your account.
You will shortly receive another email from OpenWeather with details about your current subscription, including your API key and the HTTP endpoint that we will use for communicating with the API.
Important note
The API key may take some time to be activated, usually a couple of hours, before you can use it.
As soon as the API key has been activated, we can start using it within an Angular application. We will learn how to do this in the following section.
In this section, we will create an Angular application to display weather information for a city. The user will enter the name of the city in an input field, and the application will use the OpenWeather API to get weather data for the specified city. We will cover the following topics in more detail:
Let's start by creating the Angular application first in the following section.
We will use the ng new command of the Angular CLI to create a new Angular application from scratch:
ng new weather-app --style=scss --routing=false
The preceding command will create a new Angular CLI application with the following properties:
The user should be able to enter the name of the city in an input field, and the weather information of the city should be visualized in a card layout. The Angular Material library provides a set of Angular UI components, including an input and a card, to use for our needs.
Angular Material components adhere to the Material Design principles and are maintained by the Components team of Angular. We can install the Angular Material using the following command of the Angular CLI:
ng add @angular/material --theme=indigo-pink --typography=true --animations=true
The preceding code uses the ng add command of the Angular CLI, passing additional configuration options:
./node_modules/@angular/material/prebuilt-themes/indigo-pink.css
It also includes the Material Design icons in the index.html file:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Angular Material comes with a set of predefined themes that we can use. Alternatively, we can build a custom one that fits our specific needs.
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
It adds the following class to the body of the HTML file:
<body class="mat-typography">
<app-root></app-root>
</body>
It also adds some CSS styles to the global styles.scss file of our application:
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We have nearly completed the setup and configuration of our Angular application. The last step is to add the API key that we created in the Setting up the OpenWeather API section.
The Angular CLI workspace contains the srcenvironments folder that we can use for defining application settings such as API keys and endpoints. It contains one TypeScript file for each environment that we want to support in our application. The Angular CLI creates two files by default:
Each environment file exports an environment object. Add the following properties to the object in both development and production files:
apiUrl: 'https://api.openweathermap.org/data/2.5/',
apiKey: '<Your API key>'
In the preceding snippet, the apiUrl property is the URL of the endpoint that we will use to make calls to the OpenWeather API, and apiKey is our API key. Replace the <Your API key> value with the API key that you have.
We now have all the moving parts in place to build our Angular application. In the following section, we will create a mechanism for interacting with the OpenWeather API.
The application should interact with the OpenWeather API over HTTP to get weather data. Let's see how we can set up this type of communication in our application:
ng generate interface weather
The preceding command will create the weather.ts file in the srcapp folder of our Angular CLI project.
export interface Weather {
weather: WeatherInfo[],
main: {
temp: number;
pressure: number;
humidity: number;
};
wind: {
speed: number;
};
sys: {
country: string
};
name: string;
}
interface WeatherInfo {
main: string;
icon: string;
}
Each property corresponds to a weather field in the OpenWeather API response. You can find a description for each one at https://openweathermap.org/current#parameter.
Then, we need to set up the built-in HTTP client provided by the Angular framework.
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ng generate service weather
The preceding command will create the weather.service.ts file in the srcapp folder of our Angular CLI project.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WeatherService {
constructor(private http: HttpClient) { }
}
getWeather(city: string): Observable<Weather> {
const options = new HttpParams()
.set('units', 'metric')
.set('q', city)
.set('appId', environment.apiKey);
return this.http.get<Weather>(environment.apiUrl +
'weather', { params: options });
}
The getWeather method uses the get method of the HttpClient service that accepts two parameters. The first one is the URL endpoint of the OpenWeather API, which is available from the apiUrl property of the environment object.
Important note
The environment object is imported from the default environment.ts file. The Angular CLI is responsible for replacing it with the environment.prod.ts file when we build our application.
The second parameter is an options object used to pass additional configuration to the request, such as URL query parameters with the params property. We use the constructor of the HttpParams object and call its set method for each query parameter that we want to add to the URL. In our case, we pass the q parameter for the city name, the appId for the API key that we get from the environment object, and the type of units we want to use. You can learn more about supported units at https://openweathermap.org/current#data.
Tip
We used the set method to create query parameters because the HttpParams object is immutable. Calling the constructor for each parameter that you want to pass will throw an error.
We also set the type of response data as Weather in the get method. Notice that the getWeather method does not return Weather data, but instead an Observable of this type.
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { Weather } from './weather';
The Angular service that we created contains all the necessary artifacts for interacting with the OpenWeather API. In the following section, we will create an Angular component for initiating requests and displaying data from it.
The user should be able to use the UI of our application and enter the name of a city that wants to view weather details. The application will use that information to query the OpenWeather API, and the result of the request will be displayed on the UI using a card layout. Let's start building an Angular component for creating all these types of interactions:
ng generate component weather
<app-weather></app-weather>
@NgModule({
declarations: [
AppComponent,
WeatherComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
MatIconModule,
MatInputModule,
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
Also, add the necessary import statements at the top of the file:
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { Component, OnInit } from '@angular/core';
import { Weather } from '../weather';
import { WeatherService } from '../weather.service';
@Component({
selector: 'app-weather',
templateUrl: './weather.component.html',
styleUrls: ['./weather.component.scss']
})
export class WeatherComponent implements OnInit {
weather: Weather | undefined;
constructor(private weatherService: WeatherService)
{ }
ngOnInit(): void {
}
}
search(city: string) {
this.weatherService.getWeather(city).subscribe(weather => this.weather = weather);
}
We have already finished working with the TypeScript class file of our component. Let's wire it up with its template. Open the weather.component.html file and replace its content with the following HTML code:
weather.component.html
<mat-form-field>
<input matInput placeholder="Enter city" #cityCtrl
(keydown.enter)="search(cityCtrl.value)">
<mat-icon matSuffix
(click)="search(cityCtrl.value)">search</mat-icon>
</mat-form-field>
<mat-card *ngIf="weather">
<mat-card-header>
<mat-card-title>{{weather.name}},
{{weather.sys.country}}</mat-card-title>
<mat-card-subtitle>{{weather.weather[0].main}}
</mat-card-subtitle>
</mat-card-header>
<img mat-card-image
src="https://openweathermap.org/img/wn/
{{weather.weather[0].icon}}@2x.png"
[alt]="weather.weather[0].main">
<mat-card-content>
<h1>{{weather.main.temp | number:'1.0-0'}} ℃</h1>
<p>Pressure: {{weather.main.pressure}} hPa</p>
<p>Humidity: {{weather.main.humidity}} %</p>
<p>Wind: {{weather.wind.speed}} m/s</p>
</mat-card-content>
</mat-card>
The preceding template consists of several components from the Angular Material library, including a mat-form-field component that contains the following child elements:
Important note
The cityCtrl template reference variable is indicated by a # and is accessible everywhere inside the template of the component.
A mat-card component presents information in a card layout and is displayed only when the weather component property has a value. It consists of the following child elements:
Let's now spice things up a bit by adding some styles to our component:
weather.component.scss
:host {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding-top: 25px;
}
mat-form-field {
width: 20%;
}
mat-icon {
cursor: pointer;
}
mat-card {
margin-top: 30px;
width: 250px;
}
h1 {
text-align: center;
font-size: 2.5em;
}
The :host selector is an Angular unique CSS selector that targets the HTML element hosting our component, which in our case, is the app-weather HTML element.
If we run our application using ng serve, navigate to http://localhost:4200 and search for weather information in Athens, we should get the following output on the screen:
Congratulations! At this point, you have a fully working Angular application that displays weather information for a specific city. The application consists of a single Angular component that communicates with the OpenWeather API using an Angular service through HTTP. We learned how to style our component using Angular Material and give our users a pleasant experience with our app. But what happens when we are offline? Does the application work as expected? Does the user's experience remain the same? Let's find out in the following section.
Users from anywhere can now access our Angular application to get weather information for any city they are interested in. When we say anywhere, we mean any network type such as broadband, cellular (3G/4G/5G), and Wi-Fi. Consider the case where a user is in a place with low coverage or frequent network outages. How is our application going to behave? Let's find out by conducting an experiment:
ng serve
The previous case is pretty standard in areas with low-quality internet connections. So, what can we do for our users in such areas? Luckily, the Angular framework contains an implementation of a service worker that can significantly enhance the UX of our application when running in offline mode. It can cache certain parts of the application and deliver them accordingly instead of making real requests.
Tip
The Angular service worker can also be used in environments with large network latency connections. Consider using a service worker in this type of network to also improve the experience of your users.
Run the following command of the Angular CLI to enable the service worker in our Angular application:
ng add @angular/pwa
The preceding command will transform the Angular CLI workspace accordingly for PWA support:
The configuration file is also set in the ngswConfigPath property of the build configuration in the angular.json file.
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent,
WeatherComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
MatIconModule,
MatInputModule,
MatCardModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
// Register the ServiceWorker as soon as the app is
// stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000'
})
],
providers: [],
bootstrap: [AppComponent]
})
The ngsw-worker.js file is the JavaScript file that contains the actual implementation of the service worker. It is created automatically for us when we build our application in production mode. Angular uses the register method of the ServiceWorkerModule class to register it within our application.
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
Now that we have completed the installation of the service worker, it is time to test it! Before moving on, we should install an external web server because the built-in function of the Angular CLI does not work with service workers. A good alternative is http-server:
npm install -D http-server
The preceding command will install http-server as a development dependency of our Angular CLI project.
ng build
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch –configuration
development",
"test": "ng test",
"server": "http-server -p 8080 -c-1
dist/weather-app"
}
npm run server
The preceding command will start http-server at port 8080 and will have caching disabled.
Tip
Prefer opening the page in private or incognito mode to avoid unexpected behavior from the service worker.
The service worker did all the work for us, and the process was so transparent that we could not tell whether we are online or offline. You can verify that by inspecting the Network tab:
The (ServiceWorker) value in the Size column indicates that the service worker served a cached version of our application.
We have now successfully installed the service worker and went one step closer to converting our application into a PWA. In the following section, we will learn how to notify users of the application about potential updates.
When we want to apply a change in a web application, we make the change and build a new version of our application. The application is then deployed to a web server, and every user has access to the new version immediately. But this is not the case with PWA applications.
When we deploy a new version of our PWA application, the service worker must act accordingly and apply a specific update strategy. It should either notify the user of the new version or install it immediately. Whichever update strategy we follow depends on our needs. In this project, we want to show a prompt to the user and decide whether they want to update. Let's see how to implement this feature in our application:
import { MatSnackBarModule } from '@angular/material/snack-bar';
@NgModule({
declarations: [
AppComponent,
WeatherComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
MatIconModule,
MatInputModule,
MatCardModule,
MatSnackBarModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
// Register the ServiceWorker as soon as the app
// is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000'
})
],
providers: [],
bootstrap: [AppComponent]
})
MatSnackBarModule is an Angular Material module that allows us to interact with snack bars. A snack bar is a pop-up window that usually appears on the bottom of the page and is used for notification purposes.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'weather-app';
}
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SwUpdate } from '@angular/service-worker';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'weather-app';
constructor(private updates: SwUpdate, private
snackbar: MatSnackBar) {}
}
The MatSnackBar service is an Angular service exposed from MatSnackBarModule. The SwUpdate service is part of the service worker and contains observables that we can use to notify regarding the update process on our application.
ngOnInit() {
this.updates.available.pipe(
switchMap(() => this.snackbar.open('A new version
is available!', 'Update now').afterDismissed()),
filter(result => result.dismissedByAction),
map(() => this.updates.activateUpdate().then(() =>
location.reload()))
).subscribe();
}
The ngOnInit method is an implementation method of the OnInit interface and is called upon initialization of the component. The SwUpdate service contains an available observable property that we can use to get notified when a new version of our application is available. Typically, we tend to subscribe to observables, but in this case, we don't. Instead, we subscribe to the pipe method, an RxJS operator for composing multiple operators.
import { filter, map, switchMap } from 'rxjs/operators';
A lot is going on inside the pipe method that we defined previously, so let's break it down into pieces to understand it further. The pipe operator combines three RxJS operators:
Let's see the whole process of updating to a new version in action:
ng build
npm run server
ng generate component header
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
@NgModule({
declarations: [
AppComponent,
WeatherComponent,
HeaderComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
MatIconModule,
MatInputModule,
MatCardModule,
MatSnackBarModule,
MatButtonModule,
MatToolbarModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
// Register the ServiceWorker as soon as the app is
// stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000'
})
],
providers: [],
bootstrap: [AppComponent]
})
<mat-toolbar color="primary">
<span>Weather App</span>
<span class="spacer"></span>
<button mat-icon-button>
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button>
<mat-icon>share</mat-icon>
</button>
</mat-toolbar>
.spacer {
flex: 1 1 auto;
}
<app-header></app-header>
<app-weather></app-weather>
Our Angular application has begun to transform into a PWA one. Additional to the caching mechanism that the Angular service worker provides, we have added a mechanism for installing new versions of our application. In the following section, we will learn how to deploy our application and install it natively on our device.
Firebase is a hosting solution provided by Google that we can use to deploy our Angular applications. The Firebase team has put a lot of effort into creating an Angular CLI schematic for deploying an Angular application using one single command. Before diving deeper, let's learn how to set up Firebase hosting:
Important note
Firebase generates a unique identifier for your project, such as weather-app-b11a2, underneath the name of the project. The identifier will be used in the hosting URL of your project later on.
We have now completed the configuration of the Firebase hosting. It is now time to integrate it with our Angular application. Run the following command of the Angular CLI to install the @angular/fire npm package in your Angular CLI project:
ng add @angular/fire
The preceding command will also authenticate you with Firebase and prompt you to select a Firebase project for deployment. Use the arrow keys to select the weather-app project that we created earlier and press Enter. The process will modify the Angular CLI workspace accordingly to accommodate its deployment to Firebase:
{
"hosting": [
{
"target": "weather-app",
"public": "dist/weather-app",
"ignore": [
"**/.*"
],
"headers": [
{
"source": "*.[0-9a-f][0-9a-f][0-9a-f]
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
[0-9a-f][0-9a-f].+(css|js)",
"headers": [
{
"key": "Cache-Control",
"value": "public,
max-age=31536000,immutable"
}
]
}
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
]
}
The configuration file specifies settings such as the folder that will be deployed to Firebase as stated from the public property and any rewrite rules with the rewrites property.
Important note
The folder that will be deployed by default is the dist output folder created by the Angular CLI when we run the ng build command.
"deploy": {
"builder": "@angular/fire:deploy",
"options": {}
}
To deploy the application, we only need to run a single Angular CLI command, and the Angular CLI will take care of the rest:
ng deploy
The preceding command will build the application and start deploying it to the selected Firebase project. Once deployment is complete, the Angular CLI will report back the following information:
Important note
The service worker requires an application to be served with HTTPS to work properly as a PWA, except in the localhost that is used for development. Firebase hosts web applications with HTTPS by default.
Now that we have deployed our application, let's see how we can install it as a PWA on our device:
Important note
The Install button may be found in different locations in other browsers.
The browser will prompt us to install the weather-app application.
It will also install a shortcut for launching the application directly from our device. Congratulations! We now have a full PWA application that displays weather information for a city.
In this chapter, we built a PWA application that displays weather information for a given city.
Initially, we set up the OpenWeather API to get weather data and created an Angular application from scratch to integrate it. We learned how to use the built-in HTTP client of the Angular framework to communicate with the OpenWeather API. We also installed the Angular Material library and used some of the ready-made UI components for our application.
After creating the Angular application, we introduced the Angular service worker and enabled it to work offline. We learned how to interact with the service worker and provide notifications for updates in our application. Finally, we deployed a production version of our application into the Firebase hosting and installed it locally on our device.
In the next chapter, we will learn how to create an Angular desktop application with Electron, the big rival of PWA applications.
Let's take a look at a few practice questions:
Add this before the bullet points:
44.212.50.220