Home > other >  RxJs lookup data from one array and map into another
RxJs lookup data from one array and map into another

Time:09-27

I have an angular project that's modeled after a simple eCommerce use case with Orders and Products. I have both an Order service that returns a list of Order objects. That interface looks something like this:

export interface Order {
  id: string;
  products: OrderProduct[];
}

OrderProducts look like this:

export interface OrderProduct {
  productId: string;
  quantity: number;
  price: number;
}

The last interface that's worth noting is my Product, which has the name field that I'm interested in. It looks like this:

export interface Product {
  id: string;
  name: string;
  price: number;
}

Now back in Angular, when I get into my OrderDetail component, I want to generate a little receipt or invoice, that shows the line items (aka OrderProducts) with the actual product name, and not just the ID. In order to do so, I have to look up the product from the ProductService. I'd like not to make a separate service call for each Product. Instead, I'd rather grab all products, and just have them in memory, then when I'm iterating over my Order's OrderProducts, just expand that object to include the Product's name, as looked up from my local allProducts$ observable array. Here's a snippet of what I'm working with:

  orderProducts$: Observable<OrderProduct[]> | undefined;
  allProducts$: Observable<Product[]> | undefined;
  constructor(
    private route: ActivatedRoute,
    private orderSvc: OrdersService,
    private productSvc: ProductService
  ) {}

  ngOnInit(): void {
    this.orderId$ = this.route.params.pipe(map((o) => o['orderId']));
    this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o)));
    this.orderProducts$ = this.order$.pipe(map((o) => o.products));
    this.allProducts$ = this.productSvc.fetch();
  }

Notice I have all of my products in the allProducts$ observable. What I'm looking looking to do, is create another observable that somehow marries up the associated product from the allProducts$ observable with the current orderProduct from the orderProducts$ observable, and expands it to include the Product's name (which, again, is only available in the allProducts$ observable.

If I were doing this in plain js (or ts), I'd be doing something like this:


const productsWithName = order.products.map(op => {
    const product = allProducts.find(p => p.id === op.productId);
    return {
        ...op,
        name: product?.name || '',
    }
});

Now my productsWithName array has everything I'd need. Ideally I could get that same array, as an observable array, without ever leaving Observable-land (i.e. I don't want to have to unwrap my observable to mess with them as standard JS arrays). I've tried various permutations of this with RxJs operators, but can't figure it out. I feel like I'm missing something stupid, but would appreciate a kickstart. Thanks very much in advance.

CodePudding user response:

Using combineLatest may solve this problem.

combineLatest([this.order$, this.allProducts$)).pipe(
  filter(([order, allProducts]) => !!order && !!allProducts),
  map(([order, allProducts]) => {
    return order.products.map(op => {
      const product = allProducts.find(p => p.id === op.productId);
      return {
        ...op,
        name: product?.name || '',
      }
    }) 
  }) 
)

We can subscribe the latest value of both order$ and allProducts$ and do your mapping logic inside a map operator. The filter filters out the initial undefined values.

CodePudding user response:

  1. concat two Observable
  2. groupBy productId or id
  3. o$ -> use reduce combined data
const orderProducts: Array<OrderProduct> = [
  { productId: '3', quantity: 2, price: 30 },
  { productId: '1', quantity: 1, price: 150 },
  { productId: '2', quantity: 9, price: 10 },
];
const allProducts: Array<Product> = [
  { id: '1', name: 'Apple', price: 30 },
  { id: '2', name: 'Banana', price: 150 },
  { id: '3', name: 'Cherry', price: 10 },
];

const orderProducts$ = from(orderProducts);
const allProducts$ = from(allProducts);

concat(orderProducts$, allProducts$).pipe(
  groupBy((k) => 'productId' in k ? k.productId : k.id),
  mergeMap((o$) => o$.pipe(reduce((a,c) => ({...a, ...c})))),
  toArray()
).subscribe(...);

https://stackblitz.com/edit/rxjs-teu64g

  • Related