So far, you've been working with putting together the essential elements that make up an Angular application, such as modules, components, pipes, services, RxJS, unit testing, and environment variables, and even going a step further by learning how to deliver your web application using Docker and giving it a polished look with Angular Material.
At this point, our app is not interactive. It can only pull weather information for one city. As a result, it is not a very useful app. To build an interactive app, we need to be able to handle user input. Enabling user input in your application opens up possibilities for creating great user experiences. Consider google.com's landing page:
Figure 6.1: Google's landing page
In this context, what is Google Search, apart from a simple input field with two buttons? That simple input field unlocks some of the world's most sophisticated and advanced software technologies. It is a deceptively simple and an insanely powerful way to interact with users. You can augment user input by leveraging modern web functionality such as GeoLocation
and gain new meaning from user input. So, when the user types in Paris
you don't have to guess if they mean Paris, France, or Paris, Texas, or whether you should show the current temperature in Celsius or Fahrenheit. With LocalStorage
, you can cache user credentials and remember user preferences so that you can enable dark mode in your app.
By the end of this chapter, we won't be implementing Google, GeoLocation, or dark mode, but will enable users to search for their cities using a city name or postal code (often referred to as "zip codes" in the US). Once you realize how complicated it can get implementing something as seemingly simple as a search by postal code, you may gain a new appreciation for well-designed web apps.
To build a UX driven by an input field, we need to leverage Angular forms with validation messages so that we can create engaging search experiences with search-as-you-type functionality. Behind the scenes, RxJS/BehaviorSubject enables us to build decoupled components that can communicate with one another and a reactive data stream allows us to merge data from multiple web APIs without increasing the complexity of our app.
In this chapter, you are going to learn about:
The most up-to-date versions of the sample code for the book are on GitHub at the repository linked as follows. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking for the end-of-chapter snapshot of code under the projects
folder.
For Chapter 6:
npm install
on the root folder to install dependenciesprojects/ch6
npx ng serve ch6
npx ng test ch6 --watch=false
npx ng e2e ch6
npx ng build ch6 --prod
Note that the dist/ch6
folder at the root of the repository will contain the compiled result.
Beware that the source code in the book or on GitHub may not always match the code generated by the Angular CLI. There may also be slight differences in implementation between the code in the book and what's on GitHub because the ecosystem is ever-evolving. It is natural for the sample code to change over time. Also on GitHub, expect to find corrections, fixes to support newer versions of libraries, or side-by-side implementations of multiple techniques for the reader to observe. The reader is only expected to implement the ideal solution recommended in the book. If you find errors or have questions, please create an issue or submit a pull request on GitHub for the benefit of all readers.
Next, let's see how we can implement an input field using forms. Forms are the primary mechanism that we need to capture user input. In Angular, there are two kinds of forms: reactive and template-driven. We need to cover both techniques, so that you're familiar with how forms work in Angular.
Now, we'll implement the search bar on the home screen of the application. The next user story states Display forecast information for current location, which may be taken to imply an inherent GeoLocation functionality. However, as you may note, GeoLocation is listed as a separate task. The challenge is that with native platform features such as GeoLocation, you are never guaranteed to receive the actual location information. This may be due to signal loss issues on mobile devices or the user may simply refuse to give permission to share their location information.
First and foremost, we must deliver a good baseline UX and implement value-added functionality such as GeoLocation only afterward. Instead, let's move Add city search capability ... to In progress, as shown on our Kanban board:
Figure 6.2: GitHub project Kanban board
As part of this story, we are going to implement a search-as-you-type functionality while providing feedback to the user if the service is unable to retrieve the expected data.
Initially, it may be intuitive to implement a type-search mechanism; however, OpenWeatherMap
APIs don't provide such an endpoint. Instead, they provide bulk data downloads, which are costly and are in the multiples of megabytes range.
We will need to implement our application server to expose such an endpoint so that our app can effectively query while using minimal amounts of data.
The free endpoints for OpenWeatherMap
do pose an interesting challenge, where a two-digit country code may accompany either a city name or zip code for the most accurate results. This is an excellent opportunity to implement a feedback mechanism to the user if more than one result is returned for a given query.
We want every iteration of the app to be a potentially releasable increment and avoid doing too much at any given time.
Before you begin working on a story, it is a good idea to break the story out into technical tasks. The following is the task breakdown for this story:
weather.service.ts
in order to make it more intuitive for end users to interact with our app.Let's tackle these tasks over the next few sections.
You may wonder why we're adding Angular forms since we are adding just a single input field and not a form with multiple inputs. As a general rule of thumb, any time you add an input field, it should be wrapped in a <form>
tag. The Forms
module contains the FormControl
that enables you to write the backing code behind the input field to respond to user inputs, and provide the appropriate data or the validation or message in response.
There are two types of forms in Angular:
Read more about reactive forms at https://angular.io/guide/reactive-forms.
Let's start by importing FormsModule
and ReactiveFormsModule
into our app:
src/app/app.module.ts
...
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
...
@NgModule({
...
imports: [
...
FormsModule,
ReactiveFormsModule,
]
Note that in a pure reactive form implementation, you only need the ReactiveFormsModule
. FormsModule
supports template-driven forms, and other scenarios, where you may only want to declare a FormControl
without a FormGroup
. This is how we implement the input field for this app. FormGroup
is defined in the next section.
Also, reactive forms allow you to write code in the reactive paradigm, which is a net positive. Next, let's add a city search component to our app.
We will be creating a citySearch
component using Material form and input modules:
MatFormFieldModule
and MatInputModule
to material.module.ts
so that it becomes available for use in the app:
src/app/material.module.ts
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
const modules = [..., MatFormFieldModule, MatInputModule]
We're adding MatFormFieldModule
because each input field should be wrapped in a <mat-form-field>
tag to get the most out of Angular Material functionality.
At a high level, <form>
encapsulates numerous default behaviors for keyboard, screen-reader, and browser extension users; <mat-form-field>
enables easy two-way data binding, a technique that should be used in moderation, and also allows for graceful label, validation, and error message displays.
citySearch
component:
$ npx ng g c citySearch --module=app.module
Since we added the material.module.ts
file, ng
can't guess what feature module citySearch
should be added to, resulting in an error such as More than one module matches
. Therefore, we need to provide the module that we want citySearch
to be added to, using the --module
option. Use the --skip-import
option to skip importing the component into any module.
src/app/city-search/city-search.component.html
<form>
<mat-form-field appearance="outline">
<mat-label>City Name or Postal Code</mat-label>
<mat-icon matPrefix>search</mat-icon>
<input matInput aria-label="City or Zip" [formControl]="search">
</mat-form-field>
</form>
search
and instantiate it as an instance of FormControl
:
src/app/city-search/city-search.component.ts
import { FormControl } from '@angular/forms'
...
export class CitySearchComponent implements
OnInit {
search = new FormControl()
...
Reactive forms have three levels of control:
FormControl
is the most basic element that has a one-to-one relationship with an input field.FormArray
represents repetitive input fields that represent a collection of objects.FormGroup
is used to register individual FormControl
or FormArray
objects as you add more input fields to a form.Finally, the FormBuilder
object is used to orchestrate and maintain the actions of a FormGroup
object more easily. FormBuilder
and FormGroup
are first used in Chapter 8, Designing Authentication and Authorization, and all controls, including FormArray
, are covered in depth in Chapter 11, Recipes – Reusability, Routing, and Caching.
app-city-search
to app.component.ts
as a new div
in between the row that contains the tagline of the app and the row that contains mat-card
:
src/app/app.component.ts
template: `
...
</div>
<div fxLayoutAlign="center">
<app-city-search></app-city-search>
</div>
<div fxLayout="row">
...
`,
Figure 6.3: LocalCast Weather app with a search field
If no errors occur, now we can start adding the FormControl
elements and wire them to a search endpoint.
So far, we have been passing parameters to get the weather for a city using its name and country code. By allowing users to enter zip codes, we must make our service more flexible in accepting both types of inputs.
OpenWeatherMap's API accepts URI parameters, so we can refactor the existing getCurrentWeather
function (introduced in Chapter 3, Creating a Basic Angular App) using a TypeScript union type and a type guard. That means we can supply different parameters, while preserving type checking:
getCurrentWeather
function in weather.service.ts
to handle both zip
and city
inputs:
src/app/weather/weather.service.ts
getCurrentWeather(
search: string | number,
country?: string
): Observable<ICurrentWeather> {
let uriParams = new HttpParams()
if (typeof search === 'string') {
uriParams = uriParams.set('q',
country ? `${search},${country}` : search
)
} else {
uriParams = uriParams.set('zip', 'search')
}
uriParams = uriParams.set('appid', environment.appId)
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather`,
{ params: uriParams }
)
.pipe(map(data => this.transformToICurrentWeather(data)))
}
We renamed the city
parameter to search
since it can either be a city name or a zip code. We then allowed its type to be either a string
or a number
, and depending on what the type is at runtime, we will either use q
or zip
. We also made country
optional and only append it to the query if it exists.
getCurrentWeather
now has business logic embedded into it and is thus a good target for unit testing. Following the single responsibility principle, from the SOLID principles, we will refactor the HTTP call to its own function, called getCurrentWeatherHelper
.
getCurrentWeatherHelper
.In the next sample, note the use of a backtick character, `
, instead of a single-quote character, '
, which leverages the template literals' functionality that allows embedded expressions in JavaScript:
src/app/weather/weather.service.ts
getCurrentWeather(
search: string | number,
country?: string
): Observable<ICurrentWeather> {
let uriParams = new HttpParams()
if (typeof search === 'string') {
uriParams = uriParams.set('q',
country ? `${search},${country}` : search
)
} else {
uriParams = uriParams.set('zip', 'search')
}
return this.getCurrentWeatherHelper(uriParams)
}
private getCurrentWeatherHelper(uriParams: HttpParams):
Observable<ICurrentWeather> {
uriParams = uriParams.set('appid', environment.appId)
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather`,
{ params: uriParams }
)
.pipe(map(data => this.transformToICurrentWeather(data)))
}
As a positive side effect, getCurrentWeatherHelper
adheres to the open/closed principle. After all, it is open to extension by our ability to change the function's behavior by supplying different uriParams
and is closed to modification because it won't have to be changed frequently.
To demonstrate the latter point, let's implement a new function to get the current weather by latitude and longitude.
getCurrentWeatherByCoords
:
src/app/weather/weather.service.ts
getCurrentWeatherByCoords(coords: Coordinates): Observable<ICurrentWeather> {
const uriParams = new HttpParams()
.set('lat', coords.latitude.toString())
.set('lon', coords.longitude.toString())
return this.getCurrentWeatherHelper(uriParams)
}
As you can see, getCurrentWeatherHelper
can easily be extended without any modification.
IWeatherService
with the changes made earlier:
src/app/weather/weather.service.ts
export interface IWeatherService {
getCurrentWeather(
search: string | number,
country?: string
): Observable<ICurrentWeather>
getCurrentWeatherByCoords(coords: Coordinates): Observable<ICurrentWeather>
}
As a result of adhering to the SOLID design principles, we make it easier to robustly unit test flow-control logic and ultimately end up writing code that is more resilient to bugs and is cheaper to maintain.
Now, let's connect the new service method to the input field:
citySearch
to inject the weatherService
and subscribe to input changes:
src/app/city-search/city-search.component.ts
import { WeatherService } from '../weather/weather.service'
...
export class CitySearchComponent implements OnInit {
search = new FormControl()
constructor(private weatherService: WeatherService) {}
...
ngOnInit(): void {
this.search.valueChanges
.subscribe()
}
We are treating all input as string
at this point. The user input can be a city, zip code, or a city and country code, or a zip code and country code, separated by a comma. While a city or zip code is required, a country code is optional. We can use the String.split
function to parse any potential comma-separated input and then trim any whitespace out from the beginning and the end of the string with String.trim
. We then ensure that we trim all parts of the string by iterating over them with Array.map
.
We then deal with the optional parameter with the ternary operator ?:
, only passing in a value if it exists, otherwise leaving it undefined.
src/app/city-search/city-search.component.ts
this.search.valueChanges
.subscribe(
(searchValue: string) => {
if (searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService.getCurrentWeather(
userInput[0],
userInput.length > 1 ? userInput[1] : undefined
).subscribe(data => (console.log(data)))
}
})
src/app/city-search/city-search.component.html
...
<mat-form-field appearance="outline">
...
<mat-hint>Specify country code like 'Paris, US'</mat-hint>
</mat-form-field>
...
At this point, the subscribe handler will make calls to the server and log its output to the console.
Observe how this works using Chrome DevTools. Note how often the search
function is run and that we are not handling service errors.
At the moment, we submit a request to the server with every keystroke. This is not desirable behavior, because it can lead to a bad user experience and drain battery life, resulting in wasted network requests and performance issues both on the client and server side. Users make typos; they can change their mind about what they are inputting and rarely ever do the first few characters of information input result in useful results.
We can still listen to every keystroke, but we don't have to react to every keystroke. By leveraging throttle
/debounce
, we can limit the number of events generated to a predetermined interval and still maintain the type-as-you-search functionality.
Note that throttle
and debounce
are not functional equivalents, and their behavior will differ from framework to framework. In addition to throttling, we expect to capture the last input that the user has typed. In the lodash
framework, the throttle
function fulfills this requirement, whereas, in RxJS
, debounce
fulfills it. Beware that this discrepancy may be fixed in future framework updates.
It is very easy to inject throttling into the observable stream using RxJS/debounceTime
. Implement debounceTime
with pipe
:
src/app/city-search/city-search.component.ts
import { debounceTime } from 'rxjs/operators'
this.search.valueChanges
.pipe(debounceTime(1000))
.subscribe(...)
debounceTime
will, at a maximum, run a search every second, but also run another search after the user has stopped typing. In comparison, RxJS/throttleTime
will only run a search every second, on the second, and will not necessarily capture the last few characters the user may have input.
RxJS also has the throttle
and debounce
functions, which you can use to implement custom logic to limit input that is not necessarily time-based.
Since this is a time- and event-driven functionality, breakpoint debugging is not feasible. You may monitor the network calls within the Chrome Dev Tools | Network tab, but to get a more real-time feel for how often your search handler is actually being invoked, add a console.log
statement.
It is not a good practice to check in code with active console.log
statements. As covered in Chapter 3, Creating a Basic Angular App, console.log
is a poor man's debugging method. The statements make it difficult to read the actual code, which itself bears a high cost of maintainability. So, whether they are commented out or not, do not check in code with console.log
statements.
FormControl
is highly customizable. It allows you to set a default initial value, add validators, or listen to changes on blur
, change
, and submit
events, as follows:
example
new FormControl('Bethesda', { updateOn: 'submit' })
We won't be initializing FormControl
with a value, but we need to implement a validator to disallow single character inputs:
Validators
from @angular/forms
:
src/app/city-search/city-search.component.ts
import { FormControl, Validators } from '@angular/forms'
FormControl
to add a minimum length validator:
src/app/city-search/city-search.component.ts
search = new FormControl('', [Validators.minLength(2)])
src/app/city-search/city-search.component.html
...
<form style="margin-bottom: 32px">
<mat-form-field appearance="outline">
...
<mat-error *ngIf="search.invalid">
Type more than one character to search
</mat-error>
</mat-form-field>
</form>
...
Note the addition of some extra margin to make room for lengthy error messages.
If you are handling different kinds of errors, the hasError
syntax in the template can get repetitive. You may want to implement a more scalable solution that can be customized through code, as shown:
example
<mat-error *ngIf="search.invalid">
{{getErrorMessage()}}
</mat-error>
getErrorMessage() {
return this.search.hasError('minLength') ?
'Type more than one character to search' : '';
}
search
function to not execute a search with invalid input replacing the condition in the existing if
statement:
src/app/city-search/city-search.component.ts
this.search.valueChanges
.pipe(debounceTime(1000))
.subscribe((search Value: string) => {
if (!this.search.invalid) {
...
Instead of doing a simple check to see whether searchValue
is defined and not an empty string, we can tap into the validation engine for a more robust check by calling this.search.invalid
.
For now, we're done with implementing search
functionality. Next, let's go over a what-if scenario to see how a template-driven implementation of the form would appear.
The alternative to reactive forms is template-driven forms. If you're familiar with ng-model
from AngularJS, you'll find that the new ngModel
directive is an API-compatible replacement for it.
Behind the scenes, ngModel
implements a FormControl
that can automatically attach itself to a FormGroup
. ngModel
can be used at the <form>
level or individual <input>
level. You can read more about ngModel
at https://angular.io/api/forms/NgModel.
In the Chapter 6 example code of the Local Weather app repository on GitHub, I have included a template-driven component in app.component.ts
named app-city-search-tpldriven
rendered under <div class="example">
. You can experiment with this component to see what the alternate template implementation looks like:
projects/ch6/src/app/city-search-tpldriven/city-search-tpldriven.component.html
...
<input matInput aria-label="City or Zip"
[(ngModel)]="model.search"
(ngModelChange)="doSearch($event)" minlength="2"
name="search" #search="ngModel">
...
<mat-error *ngIf="search.invalid">
Type more than one character to search
</mat-error>
...
Note the [()]
"box of bananas" two-way binding syntax in use with ngModel
.
The differences in the component are implemented as follows:
projects/ch6/src/app/city-search-tpldriven/city-search-tpldriven.component.ts
import { WeatherService } from '../weather/weather.service'
export class CitySearchTpldrivenComponent {
model = {
search: '',
}
constructor(private weatherService: WeatherService) {}
doSearch(searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService
.getCurrentWeather(userInput[0], userInput.length > 1 ?
userInput[1] : undefined
)
.subscribe(data => console.log(data))
}
}
As you can see, most of the logic is implemented in the template; as such, you are required to maintain an active mental model of the template and the controller. Any changes to event handlers and validation logic require you to switch back and forth between the two files.
Furthermore, we have lost input limiting and the ability to prevent service calls when the input is in an invalid state. It is still possible to implement these features, but they require convoluted solutions and do not neatly fit into the new Angular syntax and concepts.
Overall, I do not recommend the use of template-driven forms. There may be a few instances where it may be very convenient to use the box of bananas syntax. However, this sets a bad precedent for other team members to replicate the same pattern around the application.
To update the current weather information, we need the city-search
component to interact with the current-weather
component. There are four main techniques to enable component interaction in Angular:
This is a technique that's been leveraged since the early days of programming in general. In JavaScript, you may have achieved this with global function delegates or jQuery's event system. In AngularJS, you may have created a service and stored values in it.
In Angular, you can still create a root-level service, store values in it, use Angular's EventEmitter
class, which is really meant for directives, or use an rxjs/Subscription
to create a fancy messaging bus for yourself.
As a pattern, global events are open to rampant abuse and rather than helping to maintain a decoupled application architecture, it leads to a global state over time. A global state or even a localized state at the controller level, where functions read and write to variables in any given class, is enemy number one of writing maintainable and unit testable software.
Ultimately, if you're storing all your application data or routing all events in one service to enable component interaction, you're merely inventing a better mousetrap. This is an anti-pattern that should be avoided at all costs. In a later section, you will find that, essentially, we will still be using services to enable component interaction; however, I want to point out that there's a fine line that exists between a flexible architecture that enables decoupling and the global or centralized decoupling approach that does not scale well.
Your child component should be completely unaware of its parent. This is key to creating reusable components.
We can implement the communication between the city search component and the current weather component leveraging AppComponent
as a parent element and let the app
module controller orchestrate the data.
Commit your code now! In the next two sections you will be making code changes that you will need to discard.
Let's see how this implementation will look:
city-search
component exposes an EventEmitter
through an @Output
property:
src/app/city-search/city-search.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core'
export class CitySearchComponent implements OnInit {
@Output() searchEvent = new EventEmitter<string>()
...
this.search.valueChanges
.pipe(debounceTime(1000))
.subscribe((search Value: string) => {
if (!this.search.invalid) {
this.searchEvent.emit(searchValue)
}
})
...
}
app
component consumes that and calls the weatherService
, setting the currentWeather
variable:
src/app/app.component.ts
import { WeatherService } from './weather/weather.service'
import { ICurrentWeather } from './interfaces'
...
template: `
...
<app-city-search (searchEvent)="doSearch($event)">
</app-city-search>
...
`,
export class AppComponent {
currentWeather: ICurrentWeather
constructor(private weatherService: WeatherService) { }
doSearch(searchValue) {
const userInput = searchValue.split(',').map(s => s.trim())
this.weatherService
.getCurrentWeather(userInput[0], userInput.length > 1 ?
userInput[1] : undefined
)
.subscribe(data => this.currentWeather = data)
}
}
Note that we are binding to the searchEvent
with the parenthesis syntax. The $event
variable automatically captures the output from the event and passes it into the doSearch
method.
We successfully bubbled the information up to the parent component, but we must also be able to pass it down to the current-weather
component.
By definition, your parent component will be aware of what child components it is working with. Since the currentWeather
property is bound to the current
property on the current-weather
component, the results pass down to be displayed. This is achieved by creating an @Input
property:
src/app/current-weather/current-weather.component.ts
import { Component, Input } from '@angular/core'
...
export class CurrentWeatherComponent implements OnInit {
@Input() current: ICurrentWeather
...
}
Note that the ngOnInit
function of CurrentWeatherComponent
is now superfluous and can be removed.
You can then update the app
component to bind the data to current
weather:
src/app/app.component.ts
template: `
...
<app-current-weather [current]="currentWeather">
</app-current-weather>
...
`
At this point, your code should work! Try searching for a city. If the current-weather
component updates, then success!
The event emitter and input binding approach is appropriate in cases where you are creating well-coupled components or user controls and no outside data is being consumed. A good example might be adding forecast information to the current-weather
component, as shown:
Figure 6.4: Weather forecast wireframe
Each day of the week can be implemented as a component that is repeated using *ngFor
, and it will be perfectly reasonable for current-weather
to retrieve and bind this information to its child component:
example
<app-mini-forecast
*ngFor="let dailyForecast of forecastArray
[forecast]="dailyForecast"
>
</app-mini-forecast>
In general, if you're working with data-driven components, the parent-child or child-parent communication pattern results in an inflexible architecture, making it very difficult to reuse or rearrange your components. A good example of the tight coupling is when we imported the weather service in app.component.ts
. AppComponent
should have no idea about the weather service; its only job is to layout several components. Given the ever-changing business requirements and design, this is an important lesson to keep in mind.
Discard the changes you've made in the last two sections before moving on. We will instead be implementing an alternate solution.
Next, we cover a better way for two components to interact with each other without introducing additional coupling with subjects.
The main reason for components to interact is to send or receive updates to data either provided by the user or received from the server. In Angular, your services expose RxJS.Observable
endpoints, which are data streams that your components can subscribe to. RxJS.Observer
complements RxJS.Observable
as a consumer of events emitted by Observable
. RxJS.Subject
brings the two sets of functionalities together in an easy to work with object.
You can essentially describe a stream that belongs to a particular set of data, such as the current weather data that is being displayed, with subjects:
example
import { Subject } from 'rxjs'
...
export class WeatherService implements IWeatherService {
currentWeather$: Subject<ICurrentWeather>
...
}
currentWeather$
is still a data stream and does not simply represent one data point. You can subscribe to changes to currentWeather$
data using subscribe
, or you can publish changes to it using next
as follows:
example
currentWeather$.subscribe(data => (this.current = data)) currentWeather$.next(newData)
Note the naming convention for the currentWeather$
property, which is appended by $
. This is the naming convention for properties that are observable.
The default behavior of Subject
is very much like generic pub/sub mechanisms, such as jQuery events. However, in an asynchronous world where components are loaded or unloaded in unpredictable ways, using the default Subject
is not very useful.
There are three advanced variants of subjects:
ReplaySubject
remembers and caches data points that occurred within the data stream so that a subscriber can replay old events at any given time.BehaviorSubject
remembers only the last data point while continuing to listen for new data points.AsyncSubject
is for one-time-only events that are not expected to reoccur.ReplaySubject
can have severe memory and performance implications on your application, so it should be used with care. In the case of current-weather
, we are only interested in displaying the latest weather data received, but through user input or other events, we are open to receiving new data so that we can keep the current-weather
component up to date. The BehaviorSubject
would be the appropriate mechanism to meet these needs:
currentWeather$
as a read-only property to IWeatherService
:
src/app/weather/weather.service.ts
import { BehaviorSubject, Observable } from 'rxjs'
export interface IWeatherService {
readonly currentWeather$: BehaviorSubject<ICurrentWeather>
...
}
currentWeather$
is declared as read-only because its BehaviorSubject
should not be reassigned. Any updates to the value should be sent by calling the .next
function on the property.
BehaviorSubject
in WeatherService
and set a default value:
src/app/weather/weather.service.ts
...
export class WeatherService implements IWeatherService {
readonly currentWeather$ =
new BehaviorSubject<ICurrentWeather>({
city: '--',
country: '--',
date: Date.now(),
image: '',
temperature: 0,
description: '',
})
...
}
updateCurrentWeather
, which will trigger getCurrentWeather
and update the value of currentWeather$
:
src/app/weather/weather.service.ts
...
updateCurrentWeather(search: string | number,
country?: string): void {
this.getCurrentWeather(search, country)
.subscribe(weather =>
this.currentWeather$.next(weather)
)
}
...
IWeatherService
with the new function so that it appears as follows:
src/app/weather/weather.service.ts
...
export interface IWeatherService {
readonly currentWeather$: BehaviorSubject<ICurrentWeather>
getCurrentWeather(city: string | number, country?: string):
Observable<ICurrentWeather>
getCurrentWeatherByCoords(coords: Coordinates):
Observable<ICurrentWeather>
updateCurrentWeather(
search: string | number,
country?: string
): void
}
current-weather
component to subscribe to the new BehaviorSubject
:
src/app/current-weather/current-weather.component.ts
...
ngOnInit() {
this.weatherService.currentWeather$
.subscribe(data => (this.current = data))
}
...
city-search
component, update the getCurrentWeather
function call to utilize the new updateCurrentWeather
function:
src/app/city-search/city-search.component.ts
...
this.weatherService.updateCurrentWeather(
userInput[0],
userInput.length > 1 ? userInput[1] : undefined
)
...
Figure 6.5: Weather information for Bursa, Turkey
When you type in a new city, the component should update to include the current weather information for that city. We can move the Add city search capability... task to the Done column, as shown on our Kanban board:
Figure 6.6: GitHub project Kanban board status
We have a functional app. However, we have introduced a memory leak, so let's fix that in the next section.
Subscriptions are a convenient way to read a value from a data stream to be used in your application logic. If unmanaged, they can create memory leaks in your application. A leaky application will end up consuming ever-increasing amounts of RAM, eventually leading the browser tab to become unresponsive, leading to a negative perception of your app and, even worse, potential data loss, which can frustrate end users.
In the current-weather
component, we inject weatherSevice
so that we can access the currentWeather$
component of BehaviorSubject
. In Angular, services are singletons, meaning when they are first created in memory, they're kept alive as long as the module they're a part of is in memory. From a practical perspective, this will mean that most services in your application will live in the memory for the lifetime of the application. However, the lifetime of a component may be much shorter and there could be multiple instances of the same component created over and over again. If we don't manage the interactions between long-lived and short-lived objects carefully, we can end up with dangling references between objects, leading to memory leaks.
When we subscribe to currentWeather$
, we attach an event handler to it so that our component can react to value changes that are pushed to BehaviorSubject
. This presents a problem when the current-weather
component needs to be destroyed.
In managed languages such as JavaScript, memory is managed by the garbage collector, or GC for short, as opposed to having to allocate and deallocate memory by hand in unmanaged languages such as C or C++. At a very high level, the GC works by periodically scanning the stack for objects that are not referenced by other objects.
If an object is found to be dereferenced, then the space it takes up in the stack can be freed up. However, if an unused object still has a reference to another object that is still in use, it can't be garbage collected. The GC is not magical and can't read our minds. When an object is unused and can't be deallocated, the memory taken up by the object can never be used for another purpose so long as your application is running. This is considered a memory leak.
My colleague, Brendon Caulkins, provides a helpful analogy:
Imagine the memory space of the browser as a parking lot; every time we assign a value or create a subscription, we park a car in that lot. If we happen to abandon a car, we still leave the parking spot occupied; no one else can use it. If all the applications in the browser do this, or we do it repeatedly, you can imagine how quickly the parking lot is full, and we never get to run our application.
Next, let's see how we can ensure that we don't abandon our car in the parking lot.
Subscriptions or event handlers create references to other objects, such as from a short-lived component to a long-lived service. Granted, in our case, the current-weather
component is also a singleton, but that could change if we added more features to the app, navigating from page to page or displaying weather from multiple cities at once. If we don't unsubscribe from currentWeather$
, then any instance of current-weather
would be stuck in memory. We subscribe in ngOnInit
, so we must unsubscribe in ngOnDestroy
. ngOnDestroy
is called when Angular determines that the framework is no longer using the component.
Let's see an example of how you can unsubscribe from a subscription in the sample code in the following:
example
import { ..., OnDestroy } from '@angular/core'
import { ..., Subscription } from 'rxjs'
export class CurrentWeatherComponent implements OnInit, OnDestroy {
currentWeatherSubscription: Subscription
...
ngOnInit() {
this.currentWeatherSubscription =
this.weatherService.currentWeather$
.subscribe((data) => (this.current = data))
}
ngOnDestroy(): void {
this.currentWeatherSubscription.unsubscribe()
}
...
First, we need to implement the OnDestroy
interface for the component. Then, we update ngOnInit
to store a reference to the subscription in a property named currentWeatherSubscription
. Finally, in ngOnDestroy
, we can call the unsubscribe
method.
Should our component get destroyed, it will no longer result in a memory leak. However, if we have multiple subscriptions in a given component, this can lead to tedious amounts of coding.
Note that in city-search
, we subscribe to the valueChanges
event of a FormControl
object. We don't need to manage the subscription to this event, because FormControl
is a child object of our component. When the parent component is dereferenced from all objects, all of its children can be safely collected by the GC.
Let's now look at a better way to manage multiple subscriptions.
SubSink, published by Ward Bell, is a straightforward library to keep track of all subscriptions in a given class, whether it be a component or a service.
Add the SubSink package to your Angular project:
$ npm i subsink
Next, update current-weather
to use SubSink, replacing currentWeatherSubscription
:
src/app/current-weather/current-weather.component.ts
import { ..., OnDestroy } from '@angular/core'
import { SubSink } from 'subsink'
export class CurrentWeatherComponent implements OnInit, OnDestroy {
private subscriptions = new SubSink()
...
ngOnInit(): void {
this.subscriptions.add(
this.weatherService.currentWeather$
.subscribe((data) => (this.current = data))
)
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe()
}
...
In the preceding code sample, we instantiated a private subscriptions
object, which will serve as the sink to contain all of the subscriptions. Then, in ngOnInit
, we simply add the subscription to currentWeather$
to the sink. In ngOnDestroy
, we call unsubscribe
on the sink rather than an individual subscription.
This is a scalable approach, as the amount of boilerplate code we must write remains consistent, as you can add many subscriptions to the sink without additional coding.
Subscribing to values in data streams itself can be considered an anti-pattern because you switch your programming model from reactive to imperative. In addition, you could avoid having to manage subscriptions in the first place. We will cover this topic in the next section.
As covered in Chapter 1, Introduction to Angular and Its Concepts, we should only subscribe to an observable stream to activate it. If we treat a subscribe
function as an event handler, then we're implementing our code imperatively.
Seeing anything other than an empty .subscribe()
call in your code base should be considered a sign of ditching reactive programming.
In reactive programming, when you subscribe to an event in a reactive stream, then you're shifting your coding paradigm from reactive programming to imperative programming. There are two places in our application where we subscribe, once in current-weather
, and the other in the city-search
component.
Let's start by fixing current-weather
, so that we don't drop back into imperative programming.
Angular has been designed to be an asynchronous framework from the ground up. You can get the most out of Angular by staying in the reactive programming realm. It can feel unnatural to do so at first, but Angular provides all the tools you need to reflect the current state of your application to the user without having to shift to imperative programming.
You may leverage the async
pipe in your templates to reflect the current value of an observable. Let's update the current-weather
component to use the async
pipe:
current: ICurrentWeather
with an observable property: current$: Observable<ICurrentWeather>
.weatherService.currentWeather$
to current$:
src/app/current-weather/current-weather.component.ts
import { Observable } from 'rxjs'
export class CurrentWeatherComponent {
current$: Observable<ICurrentWeather>
constructor(private weatherService: WeatherService) {
this.current$ = this.weatherService.currentWeather$
}
...
SubSink
, ngOnInit
, and ngOnDestroy
.current$
:
src/app/current-weather/current-weather.component.html
<div *ngIf="current$ | async as current">
...
</div>
The async
pipe automatically subscribes to the current value of current$
and makes it available to the template to be used in an imperative manner as the variable current
. The beauty of this approach is that the async
pipe implicitly manages the subscription, so you don't have to worry about unsubscribing.
<div *ngIf="!current">
. This is no longer needed, because the BehaviorSubject
is always initialized.So far, the reactive style allowed us to streamline and clean up our code.
The async pipe allows you to also implement if-else
logic. If you wanted to display a loading message while your observable is resolved, you can do by using the following technique:
example
<div *ngIf="current$ | async as current;
else loading"
>
...
</div>
<ng-template #loading>
Loading...
</ng-template>
Next, let's further improve our code.
The city-search
component implements a callback within a subscribe
statement when firing the search
function. This leads to an imperative style of coding and mindset. The danger with switching programming paradigms is that you can introduce unintentional side effects to your code, making it easier to introduce errors or state into your application.
Let's refactor city-search.component.ts
to be in the reactive functional programming style, as shown in the following example:
src/app/city-search/city-search.component.ts
import { debounceTime, filter, tap } from 'rxjs/operators'
export class CitySearchComponent {
search = new FormControl('',
[Validators.required, Validators.minLength(2)])
constructor(private weatherService: WeatherService) {
this.search.valueChanges
.pipe(
debounceTime(1000),
filter(() => !this.search.invalid),
tap((searchValue: string) => this.doSearch(searchValue))
)
.subscribe()
}
doSearch(searchValue: string) {
const userInput = searchValue.split(',').map(s => s.trim())
const searchText = userInput[0]
const country = userInput.length > 1 ? userInput[1] : undefined
this.weatherService.updateCurrentWeather(searchText, country)
}
}
In the preceding code, we removed the OnInit
implementation and implemented our filtering logic reactively. The tap
operator will only get triggered if this.search
is valid. In addition, doSearch
is called in a functional context, making it very difficult to reference any other class property within the function.
This reduces the chances of the state of the class impacting the outcome of our function. As a result, doSearch
is a composable and unit testable function, whereas in the previous implementation, it would have been very challenging to unit test ngOnInit
in a straightforward manner.
Note that .subscribe()
must be called on valueChanges
to activate the observable data stream, otherwise no event will fire.
The fact that we don't implement ngOnInit
reflects the truly asynchronous nature of our code, which is independent of the life cycle or state of the application.
With our refactoring complete, the app should function the same as before, but with less boilerplate code. Now, let's look into enhancing our app so that it can handle postal codes from any country.
Currently, our app can only handle 5-digit numerical postal or zip codes from the US. A postal code such as 22201
is easy to differentiate from a city name with a simplistic conditional such as typeof search === 'string'
. However, postal codes can vary widely from country to country, Great Britain being a great example with postal codes such as EC2R 6AB
. Even if we had a perfect understanding of how postal codes are formatted for every country on earth, we still couldn't ensure that the user didn't fat-finger a slightly incorrect postal code. Today's sophisticated users expect web applications to be resilient toward such mistakes.
After the first edition of this book was published, I received some passionate reader feedback on their disappointment that the sample app can only support US zip codes. I've decided to implement this feature because it demonstrates the degree to which such seemingly simple requests can introduce unplanned complexity to your apps. As a bonus, the app now works worldwide
Let's add a new item, Support international zip codes, to the backlog and move it to In progress:
Figure 6.7: Adding an international zip codes story
To properly understand if the user inputs a valid postal code versus the name of a city, we must rely on a third-party API call provided by geonames.org. Let's see how we can inject a secondary API call into the search logic of our app.
You need to sign up for a free account on geonames.org. Afterward, store your username
as a new parameter in environment.ts
and environment.prod.ts
.
You may experiment with a postal code API on this page: https://www.geonames.org/postal-codes.
Start by implementing a PostalCodeService
, as shown in the following:
You may generate the service by executing npx ng generate service postalCode --project=local-weather-app --no-flat --lintFix
.
src/app/postal-code/postal-code.service.ts
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { defaultIfEmpty, flatMap } from 'rxjs/operators'
import { environment } from '../../environments/environment'
export interface IPostalCode {
countryCode: string
postalCode: string
placeName: string
lng: number
lat: number
}
export interface IPostalCodeData {
postalCodes: [IPostalCode]
}
export interface IPostalCodeService {
resolvePostalCode(postalCode: string): Observable<IPostalCode>
}
@Injectable({
providedIn: 'root',
})
export class PostalCodeService implements IPostalCodeService {
constructor(private httpClient: HttpClient) {}
resolvePostalCode(postalCode: string): Observable<IPostalCode> {
const uriParams = new HttpParams()
.set('maxRows', '1')
.set('username', environment.username)
.set('postalcode', postalCode)
return this.httpClient
.get<IPostalCodeData>(
`${environment.baseUrl}${environment.geonamesApi}.geonames.org/postalCodeSearchJSON`,
{ params: uriParams }
)
.pipe(
flatMap(data => data.postalCodes),
defaultIfEmpty(null)
)
}
}
Note the new environment variable, environment.geonamesApi
. In environment.ts
, set this value to api
and, in environment.prod.ts
, to secure
, so calls over https work correctly to avoid the mixed-content error, as covered in Chapter 4, Automated Testing, CI, and Release to Production.
In the preceding code segment, we implement a resolvePostalCode
function that makes a call to an API, which is configured to receive the first viable result the API returns. The results are then flattened and piped out to the subscriber. With defaultIfEmpty
, we ensure that a null value will be provided if we don't receive a result from the API. If the call is successful, we will get back all the information defined in IPostalCode
, making it possible to leverage getCurrentWeatherByCoords
using coordinates.
Let's update the weather service so that it can call the postalCode
service to determine whether the user input was a valid postal code:
src/app/weather/weather.service.ts
...
export interface IWeatherService {
...
getCurrentWeather(search: string, country?: string):
Observable<ICurrentWeather>
updateCurrentWeather(search: string, country?: string)
}
PostalCodeService
to the weather service as a private property:
src/app/weather/weather.service.ts
import {
PostalCodeService
} from '../postal-code/postal-code.service'
...
constructor(
private httpClient: HttpClient,
private postalCodeService: PostalCodeService
) {}
updateCurrentWeather
getCurrentWeather
to try and resolve searchText
as a postal code:
src/app/weather/weather.service.ts
import { map, switchMap } from 'rxjs/operators'
...
getCurrentWeather(
searchText: string,
country?: string
): Observable<ICurrentWeather> {
return this.postalCodeService.
resolvePostalCode(searchText)
.pipe(
switchMap((postalCode) => {
if (postalCode) {
return this.getCurrentWeatherByCoords({
latitude: postalCode.lat,
longitude: postalCode.lng,
} as Coordinates)
} else {
const uriParams = new HttpParams().set(
'q',
country ? `${searchText},${country}` : searchText
)
return this.getCurrentWeatherHelper(uriParams)
}
})
)
}
If you run into TypeScript issues when passing the latitude and longitude into getCurrentWeatherByCoords
, then you may have to cast the object using the as
operator. So, your code would look like:
return this.getCurrentWeatherByCoords({
latitude: postalCode.lat,
longitude: postalCode.lng,
} as Coordinates)
In the preceding code segment, our first call is to the postalCode
service. We then react to postal codes that are posted on the data stream using switchMap
. Inside switchMap
, we can observe whether postalCode
is null and make the appropriate follow-up call to either get the current weather by coordinates or by city name.
Now, LocalCast weather should work with global postal codes, as shown in the following screenshot:
Figure 6.8: LocalCast Weather with global postal codes
We are done with implementing international zip code support. Move it to the Done column on your Kanban board:
Figure 6.9: International zip code support done
As we complete our implementation of LocalCast Weather, there's still room for improvement. Initially, the app looks broken when it first loads, because of the dashes and empty fields that are shown. There are at least two different ways to handle this. The first is to hide the entire component, at the app
component level, if there's no data to display. For this to work, we will have to inject weatherService
into the app
component, ultimately leading to a less flexible solution. Another way is to enhance the current-weather
component so that it is better able to handle missing data.
You improve the app further by implementing geolocation to get the weather for the user's current location upon launching the app. You can also leverage window.localStorage
to store the city that was last displayed or the last location that was retrieved from window.geolocation
upon initial launch.
We are done with the Local Weather app until Chapter 12, Recipes — Master/Detail, Data Tables, and NgRx, where I demonstrate how NgRx compares to using RxJS/BehaviorSubject.
In this chapter, you learned how to create a search-as-you-type functionality using MatInput
, validators, reactive forms, and data stream-driven handlers. You became aware of two-way binding and template-driven forms. You also learned about different strategies to enable inter-component interactions and data sharing. You dove into understanding how memory leaks can be created and the importance of managing your subscriptions.
You are now better able to differentiate between imperative and reactive programming styles and the importance of sticking with reactive programming where possible. Finally, you learned how you can implement sophisticated functionality by chaining multiple API calls together.
LocalCast Weather is a straightforward application that we used to cover the basic concepts of Angular. As you saw, Angular is great for building such small and dynamic applications, while delivering a minimal amount of framework code to the end user. You should consider leveraging Angular for even quick and dirty projects, which is also a great practice when building larger applications. In the next chapter, you will be creating a far more complicated line-of-business (LOB) application, using a router-first approach to designing and architecting scalable Angular applications with first-class authentication and authorization, user experience, and numerous recipes that cover a vast majority of requirements that you may find in LOB applications.
After completing the Support international zip codes feature, did we switch coding paradigms here? Is our implementation above imperative, reactive, or a combination of both? If our implementation is not entirely reactive, how would you implement this function reactively? I leave this as an exercise for the reader.
Don't forget to execute npm test
, npm run e2e
, and npm run test:a11y
before moving on. It is left as an exercise for the reader to fix the unit and end-to-end tests.
Visit GitHub to see the unit tests that I implemented for this chapter at https://github.com/duluca/local-weather-app/tree/master/projects/ch6.
Answer the following questions as best as you can to ensure that you've understood the key concepts from this chapter without Googling. Do you need help answering the questions? See Appendix D, Self-Assessment Answers online at https://static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf or visit https://expertlysimple.io/angular-self-assessment.
async
pipe?