NoRyb
NoRyb

Reputation: 1544

AsyncPipe inside ngFor for items

How do I use an AsyncPipe inside an ngFor or what is best practice to accomplish asynchronous calls for items of a list?

Example: Let's say I have an Order with Products and I want to display the product's manufacturer. But the manufacturer needs to be loaded from a storage (localstorage) in order to be displayed. My initial solution would've been the following (untested pseudo-code):

HTML:

<ol>
  <li *ngFor="let prod of order.toProducts">
    {{ prod.Name }}: {{ DisplayManufacturer(prod) | async }}
  </li>
<ol>

TypeScript:

public async DisplayManufacturer(prod: product) {
    let manufacturer = await this.storageService.Load(prod.manufacturerId);
    if (manufacturer) {
        return manufacturer.name + ' (' + manufacturer.address + ')';
    }
    return "";
}

This - of course - results in an endless loop.

My next solution would be, to go through all orders and all their products inside ngOnInit to find the manufacturers and store them in a Map for later use in the expression.

I feel there should be a better solution - as this is not the end yet: I would also have to implement DoCheck to check if any of the products arrays or manufacturers change and reload said map. And this for every similarly referenced field.

Upvotes: 2

Views: 1230

Answers (2)

Dimanoid
Dimanoid

Reputation: 7279

You do not need to use async/await or load all data before displaing it. Add a field to the product type and assign a Promise or an Observable that would resolve to the required data.

service

Load(id: any): any {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`Manufacturer id [${id}], name, address`);
        }, Math.random() * 5000 + 2000);
    });
}

component

ngOnInit() {
    for (let i = 1; i < 10; i++) {
        this.order.toProducts.push(
            { Name: 'ProdName' + i, manufacturerId: i, data: this.storageService.Load('manufacturerId#' + i) },
        );
    }
}

template

<ol>
  <li *ngFor="let prod of order.toProducts">
    {{ prod.Name }}: {{ prod.data | async }}
  </li>
<ol>

Full working example: https://stackblitz.com/edit/angular-gpqvuu

Upvotes: 5

user4676340
user4676340

Reputation:

You get the products :

products$ = this.myService.getProducts();

Then the manufacturers, and join them to the products :

manufacturersAndProducts$ = this.products$.pipe(
  switchMap(products => forkJoin(products
    .map(product => this.myService.getManufacturerForProduct(product))
  )).pipe(
    map(manufacturers => products
      .map((product, index) => ({ product, manufacturer: manufacturers[index] }))
    )
  )
);

You then use this into your HTML

<ng-container *ngIf="manufacturersAndProducts$ | async as list">
  <div *ngFor="let item of list">
    {{ item.product.name }} is made by {{ item.manufacturer.name }}
  </div>
</ng-container>

But it is a bad example, because with HTTP calls you don't really need this kind of reactive programming. It is useful when, for instance, the list of products changes every 10s or so.

To answer your doubts, your question is out of context : it should not be about async, but about good practices when it comes to templates.

You should not use impure functions in your template (functions that have side effects), otherwise, as you said, your view get rendered again, and it results in infinite loops, or at best, performance issues.

You should either use a pure function, or construct a full object with all the data in it (like I've done here, but you don't need the async pipe)

Upvotes: 1

Related Questions