0
votes

A ShoppingCart with ShoppingCartItems is fetched via an outer REST call, after which an Observable of the ShoppingCartItems makes the inner call to enhance the ShoppingCartItems with a Provider.

A tap(console.log) after the inner call, reveals that the contents of the ShoppingCart are as expected - with the 5 ShoppingCartItems enhanced with a Provider. Tapping the subscription however, returns 5 alerts each containing the Provider I wanted to add as a property of ShoppingCartItem.

It seems I am using the wrong mergeMap/concatMap/switchMap - or not doing a 'collect' of some sort at the end of one or both calls.

The calls:

  getShoppingCart$(userId: number): Observable<ShoppingCart> {
    return this.rest.getShoppingCart$(userId)
      .pipe(
        mergeMap(
          (shoppingCart) => from(shoppingCart.shoppingCartItems)
            .pipe(
              concatMap(
                item => this.rest.getProviderByWine$(item.wine.id)
                  .pipe(
                    map(provider => item.provider = provider),
                  )
              ),
              // Returns ShoppingCart with Providers added
              tap(() => console.log('ShoppingCart: ' + JSON.stringify(shoppingCart)))
            )
        ),
      )
  }

The subscription:

  ngOnInit(): void {
    this.shoppingCartService.getShoppingCart$(1037).subscribe(
      (shoppingCart: ShoppingCart) => {
        this.dataSourceShoppingCart = new NestedMatTableDataSource<ShoppingCartItem>(shoppingCart.shoppingCartItems);
        // Runs 5 times - each time displaying a Provider, not the ShoppingCart
        alert(JSON.stringify(shoppingCart))
      }
    );
  }

The actual REST calls:

  getShoppingCart$(userId: number): Observable<ShoppingCart> {
    return this.http.get<ShoppingCart>(this.getBaseUrl() + 'users/' + userId + '/shopping-cart');
  }

  getProviderByWine$(wineId: number): Observable<any> {
    return this.http.get<Provider>(this.getBaseUrl() + 'wine/' + wineId + '/provider');
  }

Any pointers are greatly appreciated. Angular version is 8.

2
it seems you want to make subsequent calls where one call is dependent on result of outer call... use switchMap for that. you can read more about it here:-- medium.com/@luukgruijs/… - user1264429
That is correct - morsor

2 Answers

0
votes

Splitting the logic into multiple functions makes the reasoning easier and reduces the amount of nesting. Create a function that adds the provider to an item and a function that adds the enhanced items to a shopping cart.

You can use forkJoin to execute multiple independent observables in parallel.

getShoppingCart$(userId: number): Observable<ShoppingCart> {
  return this.rest.getShoppingCart$(userId).pipe(
    mergeMap(shoppingCart => enhanceShoppingCart(shoppingCart))
  );
}

// Add enhanced items to a shopping cart
private enhanceShoppingCart(shoppingCart: ShoppingCart): Observable<ShoppingCart> {
  // Map each shopping cart item to an Observable that adds the provider to it.
  const enhancedItems = shoppingCart.shoppingCartItems.map(item => enhanceItem(item))
  return forkJoin(enhancedItems).pipe(
    map(shoppingCartItems => ({ ...shoppingCart, shoppingCartItems }))
  );
}

// Add provider to an item
private enhanceItem(item: Item): Observable<Item> {
  return this.rest.getProviderByWine$(item.wine.id).pipe(
    map(provider => ({ ...item, provider }))
  );
}
0
votes

If there are 5 elements in the shoppingCart.shoppingCartItems array, then the observable from(shoppingCart.shoppingCartItems) will emit 5 times. That's what you're observing in the subscription.

If you're willing to replace the sequential requests using concatMap with parallel requests using forkJoin, you could try the following

getShoppingCart$(userId: number): Observable < ShoppingCart > {
  return this.rest.getShoppingCart$(userId).pipe(
    mergeMap((shoppingCart) => 
      forkJoin(shoppingCart.shoppingCartItems.map(
        item => this.rest.getProviderByWine$(item.wine.id))
      ).pipe(
        map(providers => ({
          ...shoppingCart,
          shoppingCartItems: shoppingCart.shoppingCartItems.map((item, index) => ({
            ...item,
            provider: providers[index]
          }))
        }))
      );
    ),
    tap((kart) => console.log('ShoppingCart: ' + JSON.stringify(kart)))        // <-- we are priting the output from last operator not the `mergeMap`
  );
}

After the requests we are returning the transformed shoppingCart using the map operator. You could also use concatMap with toArray() operator for sequential requests, but 5 parallel requests wouldn't be huge overhead.