0

SITUATION:

This question is regarding a SPA that I am building using Angular 12 and Ionic 5. When I am on the Home page, I can click the "Order History" link in the Side Menu and this routes me to the Order History page. I am using a Resolver in order to procure the Order History from the Database before the routing is complete, so that when the routing finishes, the User can see the data, as it is available readily via the Resolver. In this resolver, there are 2 main operations performed (strictly in order). They are:

  1. Receive the Currently Logged In User ID from Ionic Storage.

  2. Use the received Currently Logged In User ID from the above step and make a HTTP call to the backend to fetch the Orders related to the User. Only after the HTTP call finishes successfully, navigate to the "Order History" page and log the HTTP call data to console.

PROBLEM:

When I click on the "Order History" link in the Side Menu, the Resolver runs, fetches the Currently Logged in User ID from Storage, but it does not wait for the HTTP call to finish. Rather, it simply routes to the Order History page and then performs the HTTP request and then gives me the results from the HTTP request. But this beats the very purpose of the Resolver! The Resolver is supposed to wait for all the calls to finish and then navigate to the destination page, but instead, it navigates to the destination page and then finishes the API call and gives the data. I am trying to fix this so that the Resolver performs the 2 main operations as indicated above, before the actual routing occurs.

HERE IS MY CODE:

app-routing.module.ts:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { GetOrderHistoryResolver } from "@shared/resolvers/get-order-history/get-order-history.resolver";

const routes: Routes = [
  {
    path: 'order-history',
    resolve: {
      resolvedData: GetOrderHistoryResolver,
    },
    loadChildren: () => import('./order-history/order-history.module').then( m => m.OrderHistoryPageModule)
  },  
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

get-order-history.resolver.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { OrdersService } from "../../services/orders/orders.service";
import { AuthenticationService } from "@core/authentication/authentication.service";
import { Storage } from '@ionic/storage';

@Injectable({
  providedIn: 'root'
})
export class GetOrderHistoryResolver implements Resolve<any> {

  constructor(private router: Router,
              private storage: Storage,
              private authenticationService: AuthenticationService,
              private ordersService: OrdersService) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

    return this.authenticationService.getUserId().then(currentUserId => {
      console.log(currentUserId); // This works correctly and logs the value as 5
      return this.ordersService.getOrdersByCustomer(currentUserId);
    });

  }
}

authentication.service.ts

getUserId() {
  return this.storage.get('user').then(user => {
    if (user) {
      // Make sure to parse the value from string to JSON object
      let userObj = JSON.parse(user);    
      return userObj.ID;
    }
  });
}

orders.service.ts

getOrdersByCustomer(userId): any {
  return this.http.get<any>(BASE_URL + '/orders?customer=' + userId )
}

order-history.page.ts

import { Component, OnInit } from '@angular/core';
import { OrdersService } from "@shared/services/orders/orders.service";
import { ActivatedRoute } from "@angular/router";
import { Storage } from '@ionic/storage';
import { AuthenticationService } from "@core/authentication/authentication.service";

@Component({
  selector: 'app-order-history',
  templateUrl: './order-history.page.html',
  styleUrls: ['./order-history.page.scss'],
})
export class OrderHistoryPage implements OnInit {

  constructor(private route: ActivatedRoute,
              private storage: Storage,
              private ordersService: OrdersService,
              private authenticationService: AuthenticationService) {
  }

  ngOnInit() {}

  ionViewWillEnter() {
    // If the Resolver is executed, then grab the data received from it
    if (this.route.snapshot.data.resolvedData) {
      this.route.snapshot.data.resolvedData.subscribe((response: any) => {
        console.log('PRODUCTS FETCHED FROM RESOLVE');
        console.log(response); // <-- Products are successfully logged here to console
      });
    } else {
      // Make a call to the API directly because the Resolve did not work
      this.getOrdersByCustomer();
    }
  }


  /**
   * Manual call to the API directly because the Resolve did not work
   * @returns {Promise<void>}
   */
  async getOrdersByCustomer() {
    // Wait to get the UserID from storage
    let currentCustomerId = await this.authenticationService.getUserId() ;

    // Once the UserID is retrieved from storage, get all the orders placed by this user
    if(currentCustomerId > 0) {
      this.ordersService.getOrdersByCustomer(currentCustomerId).subscribe((res: any) => {
        console.log(res);
      });
    }
  }

}
Devner
  • 6,825
  • 11
  • 63
  • 104
  • Did you try to convert promise to observable and chain it with your HTTP call? – Dusan Radovanovic Jan 11 '22 at 16:34
  • I am not sure how to do that as I need the value of Currently Logged In User from the Storage first and then send that value to the HTTP call and have the Resolver wait for the response from the HTTP call. This is where I am stuck! – Devner Jan 12 '22 at 06:19

3 Answers3

3

You can convert your promise to an observable with defer from rxjs and then chain your observables in a pipe.

I am not sure if you can use from instead of defer but defer should work for sure

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return  defer(() => this.authenticationService.getUserId())
                            .pipe(switchMap((currentUserId) => 
                                     this.ordersService.getOrdersByCustomer(currentUserId)));
  }

    
Dusan Radovanovic
  • 1,599
  • 12
  • 21
  • Simple, straight and concise. Works right out of the box without needing any changes. Thank you so much!!! – Devner Jan 17 '22 at 08:25
1

I have prepared a demo for you to understand how to use the first promise response into a second one without using await, rather in the same chain of RxJS, which guarantees you that once the resolver resolve the observable, both have been evaluated:

https://stackblitz.com/edit/switchmap-2-promises?file=index.ts

Key part is here:

from(promise1())
  .pipe(
    tap((v) => console.log('Logging the 1st promise result', v)),
    // use above the first promise response for second promise call
    switchMap((v) => promise2(v)),
    tap((v) => console.log('Logging the 2st promise result', v))
  )
  .subscribe();

SwitchMap (and others high obs operators too) allows you to transform the first promise/observable output into a new one within the chain.

Alejandro Lora
  • 7,203
  • 3
  • 18
  • 34
  • Thank you for your code. I am trying to wrap my head around how to implement my code in your code. Any pointers on what/how to change the code in `get-order-history.resolver.ts` file to make it work? – Devner Jan 15 '22 at 08:30
  • You only need to replace promise1 by the first call and promise2 by the second, and the "v" by currentUserId. No need to subscribe in the resolver as the method resolve does that for you. – Alejandro Lora Jan 16 '22 at 19:15
  • Thanks a ton for your code snippet. Based on your snippet, I am sure that I was doing something wrong as I could not get my code to work. I wanted to accept both your answer and Dusan's answer and split the bounty equally to you both, but unfortunately, Stackoverflow does not let me do that. I had to accept Dusan's answer as the solution and SO is only letting me upvote your answer. I really appreciate your efforts in trying to provide the solution. Thanks you once again! – Devner Jan 17 '22 at 08:34
0

Resolve internally add handlers to returned promise/observables. If the data is fetched, it will route to the given page else it will not.

In your implementation, you are returning the Promise (Ionic Storage) & the resolver added handlers internally to this Promise instead of your HTTP Observable.

That's why 2 handlers were added. One by you that makes the HTTP call & the one added internally by the resolver. They both got executed. But resolver was only looking for the resolved value of this.authenticationService.getUserId(), it routed to the corresponding page once it got user id.

Solution: Use async/ await to get your user ID & then return the HTTP observable from the resolver.

async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

        const currentUserId=await this.authenticationService.getUserId();
        if(currentUserId){
            return this.ordersService.getOrdersByCustomer(currentUserId);
        }
        else{
            //Handle the scenario when you don't have user ID in storage
            // You can throw an error & add global error handler 
            // Or route to login / any other page according to your business needs
        }
       
      } 

Now, the resolver will add handlers to the HTTP observable returned & wait until it fetches data from BE before routing.

Pankaj Sati
  • 2,441
  • 1
  • 14
  • 21
  • Thank you for your answer. I tried your solution but it did not solve the issue. I also felt that it must have something to do with the Storage call for retrieving the Currently Logged In User ID, but wasn't sure how to solve the issue. For testing purposes, I hard-coded the User ID to 5 and the resolver worked correctly then i.e. it waited till it fetched the Order History for the User with ID 5 and then routed to the Order History page as needed. Testing the code by hard-coding the User ID proved that something needs to be done with the Storage query. So any other solution is appreciated. – Devner Jan 09 '22 at 09:52
  • Try adding await in your authentication service `getUserId()` method also – Pankaj Sati Jan 09 '22 at 11:01
  • Tried adding await as you mentioned. But still same result! Here is the new code after adding the async/await combo: `async getUserId() { return await this.storage.get('user').then(user => { if (user) { // Make sure to parse the value from string to JSON object let userObj = JSON.parse(user); return userObj.ID; } }); }` – Devner Jan 09 '22 at 11:12
  • I know that prepare a demo to reproduce it sometimes is a hassle but worth it as for sure we can share a solution with you. I think it is related to having 2 promises inside there, you could try with from(<1 promise here>).pipe(switchMap(<2 promise here>)) and handle things inside the chain. – Alejandro Lora Jan 13 '22 at 18:04
  • @AlejandroLora In your suggestion, how can I use the Output of Promise 1 as Input of Promise 2? I mean, the output of Promise 1 (Currently Logged in User ID) becomes the input for the Promise 2. How can this be handled, considering that we need the value of the Promise 1 in order to execute the Promise 2? – Devner Jan 13 '22 at 18:55
  • 1
    I shared with you in a response a link and a snippet @Devner – Alejandro Lora Jan 14 '22 at 13:48