Home > Back-end >  Angular - in subscribe push element to array
Angular - in subscribe push element to array

Time:11-09

I am making angular application and where I have an empty arrays like these :

orders: Order[];
order_details: Product[];

Then i am making a service call in ngOnInit to store the data into orders array and order_details array,

ngOnInit(): void {
    this.getOrders();
  }

This is the getOrders() function which is supposed to fetch every Order. Order object has an order_id, product_id list which has all the product ids of the products being ordered and order_time

getOrders() {
    this.order_service.getOrderList().subscribe({
      next: (data) => {
        this.orders = data;
        for (let order of this.orders) {
          for (let val of order.product_ids) {
            this.product_service.getProductById(val).subscribe({
              next: (data) => {this.order_details.push(data);}
            });
          }
        }
      },
    });
    console.log(this.order_details);
  }

getOrderList() uses api to return all orders

  getOrderList(): Observable<Order[]> {
    return this.http_client.get<Order[]>(`${this.baseURL}`);
  }

getProductById() uses api to return product by id

  getProductById(id: number): Observable<Product> {
    return this.http_client.get<Product>(`${this.baseURL}/${id}`);
  }

The Order object and Product object have fields like

export class Order{
    order_id: number;
    product_ids: number[];
    order_time: String;
}
export class Product{
    product_id: number;
    product_name: String;
    product_image: String;
    product_description: String;
    product_price: String;
}

I am trying so with the getOrders() function to fetch every order and from every order I access the product id array and find every product by id and then populate the order_details array with these products by using push()

So I was expecting a order_details array with products corresponding to the product_ids mentioned for every order in the orders array

However on doing so, error is being thrown and the order_details array is undefined

ERROR TypeError: Cannot read properties of undefined (reading 'push')

CodePudding user response:

You have not initialized empty array.

order_details: Product[] = [];

You should enable strict mode in tsconfig.json. Strict mode prevents such errors at the compiler stage.

CodePudding user response:

  1. You can edit

this.product_service.getProductById(val).subscribe({
              next: (data) => {this.order_details.push(data);}
            });

to:

this.product_service.getProductById(val).subscribe({
              next: (dataId) => {this.order_details.push(dataId);}
            });

Note: data --> dataId

  1. You should initialize in your component order_details: Product[] = [];
  2. You must check if(data) then push

CodePudding user response:

Actually I think you have another problem here, since I assume you want to have your order_details array to be in the same order as your orders array. The way your implementation works it is not guaranteed.

My suggestion is to do the following:

  1. get rid of the order_details variable and adjust your Product class as following:
export class Order{
    order_id: number;
    product_ids: number[];
    order_time: String;
    details?: Product[];
}
  1. Define orders as an Observable:
orders$: Observable<Order[]>;
  1. Use rxjs operators to "enrich" your data in parallel HTTP requests (using forkJoin):
this.orders$ = this.order_service
      .getOrderList()
      .pipe(
        switchMap((orders) =>
          forkJoin(
            orders.map((order) =>
              forkJoin(
                order.product_ids.map((product_id) =>
                  this.product_service.getProductById(product_id)
                )
              ).pipe(map((products) => ({ ...order, details: products })))
            )
          )
        )
      );

This will lead to the order objects details property to be "enriched" with the data coming from this.product_service.getProductById(product_id).

  1. Use async pipe to listen to the output of orders$ observable.

CodePudding user response:

Your error is just from not initializing your arrays:

orders: Order[] = [];
order_details: Product[] = [];

Note that your current setup will not maintain the order that product details were requested, they will randomly be pushed to the array as soon as a request completes.

Suggestions below.


Simple Example: https://stackblitz.com/edit/angular-ivy-2kfwbz?file=src/app/app.component.ts

Optimized Example: https://stackblitz.com/edit/angular-ivy-2whd3b?file=src/app/app.component.html

Before subscribing to observables, you should create a pipeline that transforms the data as necessary.

orders is just the result of this.order_service.getOrderList() so we can initialize it as such. Some people tend to annotate observables with a $.

export class MyComponent {
  orders$: Observable<Order[]> = this.order_service.getOrderList();

  constructor(
    private order_service: OrderService,
    private product_service: ProductService
  ) {}
}

order_details depends on the result of orders$ so you can create a pipeline with that as the starting point.

order_details$: Observable<Product[]> = this.orders$.pipe(
    switchMap((orders) => {
      const res: Observable<Product>[] = [];
      for (let o of orders) {
        for (let id of o.product_ids) {
          res.push(this.product_service.getProductById(id));
        }
      }
      return forkJoin(res);
    })
  );

Note the use of switchMap and forkJoin.

forkJoin takes an array of observables, waits for all of them to complete, then emits an array with the completed values. Note they do need to complete, if you are using long lived observables, you can use combineLatest instead.

switchMap is necessary since forkJoin returns an observable, and pipe also returns an observable. This would create a nested observable if we were just using map. switchMap resolves the inner observable.

To display the values in html you can use the async pipe, which automatically subscribes / unsubscribes. It's generally best practice, but not always realistic.

<h1>Orders</h1>
<pre>{{ orders$ | async | json }}</pre>
<h1>Order Details</h1>
<pre>{{ order_details$ | async | json }}</pre>

Note that this isn't optimized since both observables are making a duplicate request for orders$. But it is nice and simple.

To eliminate the duplicate, you can save the result of orders, and then after it has been populated, create the pipeline for order_details

  orders: Order[] = [];
  order_details$: Observable<Product[]> = new Observable();

  ngOnInit() {
    this.order_service.getOrderList().subscribe((res) => {
      this.orders = res;
      this.getOrderDetails();
    });
  }

  getOrderDetails() {
    const res: Observable<Product>[] = [];
    for (let o of this.orders) {
      for (let id of o.product_ids) {
        res.push(this.product_service.getProductById(id));
      }
    }
    this.order_details$ = forkJoin(res);
  }
<h1>Orders</h1>
<pre>{{ orders | json }}</pre>
<h1>Order Details</h1>
<pre>{{ order_details$ | async | json }}</pre>

Of course you can subscribe and save the result to a local variable if you want.

order_details: Product[] = [];

getOrderDetails() {
    const res: Observable<Product>[] = [];
    for (let o of this.orders) {
      for (let id of o.product_ids) {
        res.push(this.product_service.getProductById(id));
      }
    }
    forkJoin(res).subscribe((res) => this.order_details = res)
  }

Note: I'm assuming these observables are the result of simple http requests, so unsubscribing is unnecessary

  • Related