data synchronization
Angular Asynchronous Patterns RxJS SPA Top Typescript Visual Code

Using Data Resolvers in Angular Components

Welcome to today’s post.

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

What are and when do we use 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.

Below is an architecture showing how this works:

Angular resolvers

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?

It is 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.

For more details on the ForkJoin() RxJS operator refer to the following.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial