[Fixed] Angular – how to map observable inside array

Issue

I have an array of servers object where inside that array I have another observable array of objects, its key is [securityGroups].

ngOnInit(): void {
    forkJoin(
      this.serverService.getServer(),
      this.securityGroupService.getSecurityGroups())
      .pipe(takeWhile(() => this.alive))
      .subscribe(([servers, groups]) => {
        this.servers = servers.map((item) => ({
          ...item,
          securityGroups: this.serverService.getServerById(item.id)
            .pipe(map(server => server["security_groups"]))
        }))
        this.securityGroupArray = groups['security_groups'].map((item) => ({
          ...item,
          expanded: false,
        }))
      }

How can I map this [securityGroup] key from my server array? since it is an Observable. I would not like to make an asynchronous pipe in html, I would like to save it in a new array

My servers Array payload:

[{id: "1879f47f-1c5e-464b-bb76-e7cc13ef426e", name: "hello", flavor: {…}, securityGroups: Observable}
,
{id: "b9c7e32a-bf99-4250-83cb-13523f9c1604", name: "test01", flavor: {…}, securityGroups: Observable}]

Solution

If I understand correctly, your challenge is that you receive an array of servers and you need to make a call for each one to retrieve more data (securityGroups) and append it to the object.

To accomplish this, you’ll need to "subscribe" to this secondary call somehow, so that you can receive the data. There are several "Higher Order Mapping Operators" that can do this for you, so you don’t have to deal with nested subscriptions. In this case, we can use switchMap.

In order to make a bunch of calls at one time, we can use forkJoin to make all the calls at once and receive an array of all the results.

You’re currently using forkJoin to make 2 different calls, but you aren’t using the results from those calls for the same purpose. I would split them up into separate observables, both for clarity of intent and so they can be used independently:

  securityGroups$ = this.securityGroupService.getSecurityGroups().pipe(
    map(groups => groups.map(group => ({
      ...group,
      expanded: false
    })))
  );

  servers$ = this.serverService.getServers().pipe(
    switchMap(servers => forkJoin(
      servers.map(s => this.serverService.getServerById(s.id))
    )
    .pipe(
      map(serversWithGroups => serversWithGroups.map((server, i) => ({
        ...servers[i],
        securityGroups: server.security_groups
      }))),
      // shareReplay(1) - see comment below
    ))
  );

You could still subscribe in your controller if you want:

ngOnInit() {
    this.servers$.subscribe();
    this.securityGroups$.subscribe();
}

or your could use the AsyncPipe in the template:

<ng-container *ngFor="let group of securityGroups$ | async">
    <option *ngFor="let server of servers$ | async" [value]="server.id">
        <ng-container *ngFor="let secGroup of server.securityGroups">
            {{ secGroup.name !== group.name ? server.name : '' }}
        </ng-container>
    </option>
</ng-container>

If you’re nesting *ngFor then you can use the shareReplay() operator to prevent multiple subscriptions from executing your service methods multiple times.

Leave a Reply

(*) Required, Your email will not be published