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:
- 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
- You should initialize in your component order_details: Product[] = [];
- 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:
- get rid of the
order_details
variable and adjust yourProduct
class as following:
export class Order{
order_id: number;
product_ids: number[];
order_time: String;
details?: Product[];
}
- Define orders as an Observable:
orders$: Observable<Order[]>;
- Use
rxjs
operators to "enrich" your data in parallel HTTP requests (usingforkJoin
):
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)
.
- Use
async
pipe to listen to the output oforders$
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