0
votes

Right now I'm using forkJoin on an array of observables, which works well. However, I need to associate each observable with another property so that I can track the property after subscribing to the forkJoin.

So right now, my array of values:

randoArray = ["3", "4", "8", "9"];

Creating the array of observables and returning the forkJoin:

this.randoArray.forEach(key => {
  this.observableBatch.push(this.getDocument(key));
});

return forkJoin(this.observableBatch);

But what I need to do is instead have an array of objects, so for each object there would be one property for the name and one property for the item I need to create an observable for... this way once I subscribe I get the result of the observable along with a name for it.

  newArray = [
    { name: "testName", value: "3" },
    { name: "testName2", value: "4" },
    { name: "testName3", value: "8" },
    { name: "testName4", value: "9" }
  ];

this.newArray.forEach(item => {
  this.observableBatch.push({ observable: this.getDocument(item.key), name: item.name });
});

But I can't run a forkJoin on the array of objects because it's now an array of objects with properties that aren't just observables, ie I can't do this:

return forkJoin(this.observableBatch);

I'm not sure how to do this. I need to forkJoin just the observables in an array of objects and somehow keep track of each name property.

StackBlitz: https://stackblitz.com/edit/angular-ivy-nkunke?file=src/app/app.component.ts

Full code:

export class AppComponent {
  constructor(private http: HttpClient) {}

  randoArray = ["3", "4", "8", "9"];

  // I want to keep the name property and associate them w/ each observable
  newArray = [
    { name: "testName", value: "3" },
    { name: "testName2", value: "4" },
    { name: "testName3", value: "8" },
    { name: "testName4", value: "9" }
  ];

  observableBatch = [];
  display = "";

  test2() {
    this.getObservables().subscribe(data => {
      console.log(data);
      this.display = JSON.stringify(data);

      // reset
      this.observableBatch = [];
    });
  }

  getObservables() {
    this.randoArray.forEach(key => {
      this.observableBatch.push(this.getDocument(key));
    });

    console.log(this.observableBatch);

    return forkJoin(this.observableBatch);
  }

  getDocument(id: string) {
    return this.http.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
  }
}

Any help would be much appreciated.

3

3 Answers

3
votes

You can just map the http request result observable to something else. (ie. some object with original item.)

You can pipe methods onto any observable, anywhere in the stream.

    // Map id array to http requests
    const requests = this.newArray.map(item => {
      // Return the request
      return this.getDocument(item.value).pipe(
        map(result => {
          // map the result of that requests to some object?
          return {
            originalItem: item,
            requestsResult: result
          };
        })
      );
    });

    return forkJoin(requests);

stackblitz

1
votes

Do you really need to use a forkJoin ? Because this what is leading you to headaches. Here is how I would do it without a forkjoin, and here is the code :

.ts:

import { Component, VERSION } from "@angular/core";
import { BehaviorSubject, forkJoin, of } from "rxjs";
import { HttpClient } from "@angular/common/http";
import { catchError, tap } from "rxjs/operators";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  constructor(private http: HttpClient) {}

  randoArray = ["3", "4", "8", "9"];

  newArray = [
    { name: "testName", value: "3" },
    { name: "testName2", value: "4" },
    { name: "testName3", value: "8" },
    { name: "testName4", value: "9" }
  ];

  mappedNewArray = new Array();

  getData() {
    this.randoArray.forEach(key => {
      this.getDocument(key);
    });
  }

  getDocument(id: string) {
    this.http
      .get(`https://jsonplaceholder.typicode.com/todos/${id}`)
      .pipe(
        tap(data => {
          console.log("data = ", data);
          const item = this.newArray.find(item => item.value === id);
          this.mappedNewArray.push({ data, item });
        }),
        catchError(err => {
          console.error("Error ! Abort the mission ! ", err);
          return of(err);
        })
      )
      .subscribe();
  }
}

.html:

<button (click)="getData()">Test</button>

<div *ngIf="mappedNewArray && mappedNewArray.length === randoArray.length">
    <pre>{{ mappedNewArray | json }}</pre>
</div>

I made some changes since I don't use observables by myself here. Your data are coming directly from the http request, so I just treat it directly after receiving the result.

You can probably use a BehaviorSubject instead of a simple array for the mappedNewArray if you need to update your data later.

1
votes
this.randoArray.forEach(key => {
  this.observableBatch.push(this.getDocument(key));
});

return forkJoin(this.observableBatch);

becomes

this.observableBatch = this.randoArray.map(key => 
  this.getDocument(key).pipe(
    doc => ({key, doc})
  )
);

return forkJoin(this.observableBatch);

Or in one line:

return forkJoin(
  this.randoArray.map(key => 
    this.getDocument(key).pipe(
      doc => ({key, doc})
    )
  )
);

Either way, you're now emitting an array of objects that hold both the key from randoArray and the document from this.getDocument(key)