data synchronization
Angular Asynchronous Patterns RxJS SPA Top Typescript Visual Studio Code

How to Use Data Resolvers in Angular Components

Welcome to today’s post.

I will discuss how to implement Resolvers within your Angular components.

What are Resolvers?

Resolvers are essentially a feature of the Angular routing core library that allows us to load and combine data from various sources (API services, databases, files etc.) and inject this data into the routing snapshot data. From the routing snapshot data, we can obtain the necessary page data we wish to use within our Angular component.

When do we use Resolvers?

As a guide, resolvers are most suitably applied in Angular applications that contain components that are dependencies of other components. The dependencies are then treated as input parameters of other components. When the input parameters are simple types, such as integers or strings, and they are not required to lookup any other data within the component’s initialization, then Resolvers are unlikely to be required. When the input parameters are required to lookup additional data that is then combined within the component’s initialization, then Resolvers would be of use.  

To reinforce the idea, I will later walk through an example of how Resolvers are used to combine two sets of data into an Angular component.

Below is an architecture showing how this works:

Angular resolvers

Typical component types that would be good beneficiaries of the Resolver pattern are:

  1. Graph components that use data for the main report and data for the axes.
  2. Grid based reports that require data from multiple data sources in the row data and in detailed column data.

Implementation steps for an Angular resolver

To implement a Resolver within our Angular application, we need to complete the following tasks:

  1. Determine the data dependencies for the target component.
  2. Create a resolver class.
  3. Declare the resolver class within the route for the target component.
  4. Declare the Resolver as a Provider
  5. Consume the router snapshot data within the target `component within ngInit().

I will go through each task in turn.

Data dependencies

We have a basic UI component that is a form detail. The form takes a parameter id, which is used to load the data record with the same primary key from a Book table via an API service. The form also contains a Genre lookup, the data source being from a Lookup service.

Creating a Resolver class

With a resolver class, we will need to declare and implement a function resolve():

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>

which will take the route snapshot and router state and return the combined data which is an observable.

We make use of the RxJs forkJoin()  function in order to synchronize the loading of the data sources then return them as page data through the routing snapshot data. The code is shown below:

import { Injectable } from '@angular/core';
import { RouterStateSnapshot, ActivatedRouteSnapshot, 
  Resolve,  Router } from '@angular/router';
import { Observable, forkJoin } from 'rxjs';
import { map } from 'rxjs/internal/operators/map';
import { LookupService } from '../services/lookup.service';
import { ApiService } from '../services/api.service';

@Injectable()
export class BookDetailComponentPageResolver implements 
  Resolve<{ any }> {
    constructor(private router: Router,
        private lookupSvc: LookupService,
        private apiSvc: ApiService
    ) { }
    resolve(route: ActivatedRouteSnapshot, 
state: RouterStateSnapshot): Observable<any> {
        let id = route.paramMap.get("id");
        return forkJoin(
        [
           this.apiSvc.getBook(id),	
           this.lookupSvc.getGenres()
        ])
        .pipe(map(
            resp => 
            {             
              return {
                id: id,
                title: resp[0].title,
                author: resp[0].author,
                yearPublished: resp[0].yearPublished,
                genre: resp[0].genre,
                edition: resp[0].edition, 
                isbn: resp[0].isbn,
                location: resp[0].location,
                genres: resp[1]                
              }
        ));
    }    
}

Before we get around to the next task we can ask the following question:

How does the forkJoin() function work?

With the forkJoin() function, we start by taking multiple observables as input:

forkJoin(
[
	observable 1,
	observable 2,
	…
	observable N,
])

When they have all completed, they will be mapped to a final array:

resp[0], resp[1], … resp[N], 

where

resp[i] = result of observable i

From the final response array, we can selectively return the properties we wish to use in our target component.

Declare the resolver class within the route for the target component

Our resolver class is declared within the app routing module with the resolve property of the Routes array assigned to the page resolver component.  

This is shown in the code below:

app-routing.module.ts

import { BookDetail2Component } from 
  './book-detail-2/book-
detail2.component';
import { BookDetailComponentPageResolver } 
  from './book-detail-2/book-detail-2.component.resolver';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'prefix'},
	…
  { path: 'book-detail-2/:id', component: BookDetail2Component, 
resolve: { pageData: BookDetailComponentPageResolver } },
];

Declare the Resolver as a Provider

In your app module, declare the page resolver component as a provider.

app.module.ts

import { BookDetailComponentPageResolver } 
  from './book-detail-2/book-
detail-2.component.resolver';

@NgModule({
  declarations: [
	…
  ],
  imports: [
	…
  ],
  providers: [
	…
    BookDetailComponentPageResolver
  ],
  bootstrap: [AppComponent]
})

Consume the router snapshot data within the target component within ngInit()

this.id = this.activatedRoute.snapshot.params["id"];

try {
  this.id = this.route.snapshot.data.pageData.id;
  this.book = new Book();
  this.book.title = this.route.snapshot.data.pageData.title;
  this.book.author = this.route.snapshot.data.pageData.author;
  this.book.yearPublished = 
    this.route.snapshot.data.pageData.yearPublished;
  this.book.genre = this.route.snapshot.data.pageData.genre;
  this.book.edition = this.route.snapshot.data.pageData.edition;
  this.book.isbn = this.route.snapshot.data.pageData.isbn;
  this.book.location = this.route.snapshot.data.pageData.location;
  this.genres = 
    this.route.snapshot.data.pageData.genres.map(g => g.genre);

When the application is run and the detail form is displayed, the detail record and lookup should be displayed as shown:

From the above, we can see how powerful and useful resolvers are within Angular forms that consume multiple data sources, and how they contribute towards the responsiveness of our application UI components.

More details on the ForkJoin() operator can be obtained on the following RxJS site.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial