To demonstrate the versatility of RxJS in an Angular application, we will perform the task of adding a search for exercises in our backend to the diary entry inclusion form.
Following the good practices of an Angular application, we will create an interface that will represent the exercises. From the command line of the operating system, we will use the Angular CLI:
ng g interface diary/interfaces/exercise
In the file generated by the Angular CLI, we define the structure of the API return:
export interface Exercise {
id?: string;
description: string;
}
export type ExerciseList = Array<Exercise>;
export interface ExerciseListAPI {
hasNext: boolean;
items: ExerciseList;
}
We are using interfaces to define the return of the API and a type to define a list of exercises. The next step is to create the service that will fetch this information, again using the Angular CLI:
ng g service diary/services/exercises
With the structure of the service created by the Angular CLI, we will complete the logic of the service:
export class ExercisesService {
private httpClient = inject(HttpClient);
private url = ‘exercises’;
getExercises(filter?: string): Observable<ExerciseList> {
const headers = new HttpHeaders().set(‘X-LOADING’, ‘false’);
filter = filter ?
`?filter=${filter}` : ”;
return this.httpClient
.get<ExerciseListAPI>(`${this.url}${filter}`, { headers })
.pipe(map((api) => api?.items));
}
}
In the service, we are using the HttpClient Angular service because we are going to query an API, and we are adding the X-LOADING header with false to the request because, here, we don’t want the loading screen to search for exercises.
If the component passes a filter, we will add the get URL. Finally, we are using the map operator that we saw in the previous section because we don’t want the component to worry about knowing the structure of the API.
With the service created, we can change the NewEntryFormReactiveComponent form:
export class NewEntryFormReactiveComponent implements OnInit {
private exerciseService = inject(ExercisesService);
public showSuggestions: boolean = false;
public exercises$ = this.exerciseService.getExercises();
selectExercise(suggestion: string) {
this.entryForm.get(‘exercise’)?.setValue(suggestion);
this.toggleSuggestions(false);
}
toggleSuggestions(turnOn: boolean) {
this.showSuggestions = turnOn;
}
}
Here we are first injecting the service we created and creating an attribute to control when to show the exercises list or not.
The exercises$ attribute will contain the observable that the service will return. One detail that you may have noticed is the $ symbol here. Using this postfix for variables and attributes that are observables is a community convention. It is not an obligation, but you will often see this symbol in code bases that use RxJS.
We also created two methods that will be triggered when the user selects an exercise from the list. Let’s change the form template:
<input
type=”text”
id=”exercise”
name=”exercise”
class=”w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow”
formControlName=”exercise”
(focus)=”toggleSuggestions(true)”
/>
<ul
class=”absolute z-10 mt-2 w-auto rounded border border-gray-300 bg-white”
*ngIf=”showSuggestions”
>
<li
*ngFor=”let suggestion of exercises$ | async”
class=”cursor-pointer px-3 py-2 hover:bg-blue-500 hover:text-white”
(click)=”selectExercise(suggestion.description)”
>
{{ suggestion.description }}
</li>
</ul>
In the exercise field, we are adding a list with the ul HTML element, and this list will be presented by the showSuggestions attribute. The focus event of the field will trigger this variable and clicking on the element will call the selectExercise method.
The attention in this code will be on the following directive:
*ngFor=”let suggestion of exercises$ | async”
With the *ngFor directive, we want to iterate over a list, but here, we don’t have a list but an observable. How is that possible?
This is the responsibility of the async pipe! What this pipe does in the template is perform a subscription in the observable, take the result of it, which is a list of exercises, and offer the *ngFor directive to the iteration.
Notice that we only got such concise code because, in the service, we are using the map operator to prepare the return of the observable for exactly what the component needs. Another advantage that the async pipe provides is that the framework controls the life cycle of the observable; that is, when the component is destroyed, Angular automatically triggers the unsubscribe method.
We haven’t done this treatment so far in the book because the observable generated by an HTTP request is not open after the request is completed, but here we will use observables for other cases that may leave the flow with the observable still open.
It is very important to control the life cycle of the observables that we use; otherwise, we can generate bugs and performance degradation caused by memory leaks. Using the async pipe, this subscription management is done by Angular itself!
In the next section, we will connect different streams using RxJS and the async pipe.