4

The goal is to iterate through a collection of IDs, making an HTTP call for each ID. For each ID, I'm using a service with a get() method that returns an Observable. Each time the get() method is called, I'm subscribing to the returning Observable and trying to push the result into an array, which will eventually get passed on to a different method for a new operation.

Relevant service method:

public get(departmentId: number): Observable<IDepartmentModel> {
    return super.get<IDepartmentModel>(departmentId);
}

note: the super class is leveraging Angular Http, which is well tested and confirmed to be working correctly. The problem with the logic isn't here...

Relevant component methods: note the departmentService.get() call that's being called several times within the forEach.

 setInitialDepartmentsAssignedGridData(): void {
    this.settingsForDropdownSelectedCompanyId = this.userForm.get('defaultCompany').get('defaultCompanyId').value;

    let departments: IDepartmentModel[] = [];

    this.userService.user.getValue() //confirmed: valid user is being pulled back from the userService (logic is fine here..)
        .userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId) // getting a valid match here (logic is fine here..)
        .departmentIds.forEach(deptId => this.departmentService.get(deptId).first().subscribe(dept => { // getting a valid department back here (logic is fine here...)
            departments.push(dept); // HERE LIES THE PROBLEM
    }));

    this.setDepartmentsAssignedRowData(departments);
}

 setDepartmentsAssignedRowData(departments: IDepartmentModel[]): void {
    console.log('setDeptAssignedRowData called'); // confirmed: method is getting called...
    console.log(departments); // confirmed: fully-composed collection of departments logged to the console...

    departments.forEach(dept => {
        console.log(dept);
    }); // Y U NO WORK!?

    departments.map((department) => {
        console.log(department); // Y U NO WORK?
        this.departmentAssignedRowData.push({
            departmentId: department.id,
            departmentName: department.name
        });
    });

    this.departmentAssignedGridOptions.api.setRowData(this.departmentAssignedRowData);
}

The problem is, although what's getting logged to the console is a fully-composed department-objects array, it's not TRULY "there"; what's getting passed to setDepartmentsAssignedRowData is an empty array.

I'm sure what's happening is that the async operations are not complete before the departments array gets passed to the second method. Some of what I've read online says to use forkJoin, but I can't see how that will look in this context. I've also read concatMap may work, but again, in this context, I'm not sure how to make that work...

In this context, how do I leverage RxJS to make sure the intended, fully-composed departments array is truly ready to be passed?

thanks for any insight you can provide. help is much appreciated!

Steve Boniface
  • 571
  • 1
  • 11
  • 23

1 Answers1

5

You are correct, you need forkJoin

let observableArray = this.userService.user.getValue()
    .userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId)
    .departmentIds.map(deptId => this.departmentService.get(deptId)) // map is building out an array of observables

This will be an array of http request observables that you want to make in parallel. Now you can pass this array to forkJoin.

Observable.forkJoin(...observableArray)

The return of forkJoin will be an array of results from observableArray. forkJoin will not emit to the next operator in the sequence until all of the observables in observableArray have completed (so when all of the http requests have finished)

So altogether the code will be

let observableArray = this.userService.user.getValue()
    .userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId)
    .departmentIds.map(deptId => this.departmentService.get(deptId));

Observable.forkJoin(...observableArray).subscribe(res => {
    // res = [resId0, resId1, resId2, ..., resIdX];
});

You mentioned passing the result to another operator. If that operator is another http request where you pass an array of data (from forkJoin), then you can use the flatMap operator.

Observable.forkJoin(...observableArray)
    .flatMap(res => {
        return this.otherApi(res);
    })
    .subscribe(res => {
        // res is the result of the otherApi call
    });

flatMap will chain your api requests together. So altogether what is happening is

  1. run array of observables in parallel
  2. once complete, run second api (otherApi)
LLai
  • 13,128
  • 3
  • 41
  • 45
  • Awesome, I will give this a try; thank you! One question: doesn’t forkJoin require more than one param? I’d just pass observableArray and nothing else to Observable.forkJoin? – Steve Boniface Dec 09 '17 at 01:50
  • @SteveBoniface forkJoin also takes a `selector` parameter, which is a function that modifies the result. But it is optional and not needed in this case. It's more of an organizational thing. You can do the same modification in the next operator – LLai Dec 09 '17 at 02:07
  • 1
    Oh, sure. I won't be needing that for this context. I tried your suggestion (will edit my initial post to see the finished solution. Can't thank you enough for your insight. This has saved me a headache that has lasted 2-3 days now :). Kudos! Thank you – Steve Boniface Dec 09 '17 at 15:46
  • Just what I needed. Can't thank you enough. Thank you so much – Alf Moh Aug 11 '18 at 10:00