3

I am fairly new to Observable so I believe there must be a method to bypass my for each loop.

this.pages.forEach(page => {
  page.tasks = [];
  page.image = this.getImageData(this.proof.proofGuid, page.pageGuid);
  page.tasks = this.getPageTasks(this.order.orderNum);
});

Both functions subscribe to an observable from my service but since they are async the for each loop is finished before the data has been returned and put on the object.

getImageData(proofGuid, pageGuid) {
  this.proofService.getProofImage(proofGuid, pageGuid)
    .subscribe(image => {
      return image;
    });
}

getPageTasks(orderNum) {
  this.taskService.getTasksByOrderIdForProof(orderNum)
    .subscribe(data => {
      if (data) {
        return data;
      }
    });
}

Right now I do not have any other option but to loop to get the data of each page. I believe there has to be a way to basically force the subscription to finish before I move on to the next page in the for each.

1 Answer 1

5

your use of observables / understanding of asynchronous execution needs some clearing up.

First this:

getImageData(proofGuid, pageGuid) {
this.proofService.getProofImage(proofGuid, pageGuid)
  .subscribe(image => {
    return image;
  });
}

getPageTasks(orderNum) {
this.taskService.getTasksByOrderIdForProof(orderNum)
  .subscribe(data => {
    if (data) {
      return data;
    }
  });
 }

doesn't make sense. You're not actually returning anything ever. These functions will execute when called and then the value youu "returned" will be thrown out. Because the subscribe function does not return. its interface defines it as returning void, so calling return inside of it is useless. Instead you should return the stream itself and call subscribe where you want the value:

getImageData(proofGuid, pageGuid) {
  return this.proofService.getProofImage(proofGuid, pageGuid);
}

getPageTasks(orderNum) {
  return this.taskService.getTasksByOrderIdForProof(orderNum);
}

Much better. Now for your execution.

this.pages.forEach(page => {
  page.tasks = [];
  page.image = this.getImageData(this.proof.proofGuid, page.pageGuid);
  page.tasks = this.getPageTasks(this.order.orderNum);
});

this doesn't make sense anymore. it would more accurately be like:

this.pages.forEach(page => {
  page.tasks = [];
  this.getImageData(this.proof.proofGuid, page.pageGuid).subscribe(image => page.image = image);
  this.getPageTasks(this.order.orderNum).subscribe(tasks => page.tasks = tasks);
});

It may actually work just fine at this point. But you are right that you can combine observable and all that, you could do:

this.pages.forEach(page => {
  page.tasks = [];
  Observable.forkJoin(this.getImageData(this.proof.proofGuid, page.pageGuid), 
      this.getPageTasks(this.order.orderNum))
      .subscribe(([image, tasks]) => {
          page.image = image;
          page.tasks = tasks;
      });
});

The forkJoin operator will execute asynchronous calls in parallel and give you the results in a matching array when both have completed.

You could get far more creative / efficient and only execute the tasks call once since it looks like the input param never changes, and you could combine all the image calls into one parallel stream.

It's important to remember that observables are just a definition of how you handle a stream of data and don't actually do anything till you subscribe. so you could do stuff like this:

getFullPages(orderNum, pages) {
  // define simple stream to retrieve tasks
  let tasks$ = this.getPageTasks(orderNum); 

  // map pages array into an array of streams to fetch image and add image to page and return the page w the image and join them all
  let pageImages$ = Observable.forkJoin(
                       pages.map(page => // be careful, this is the array map operator
                            this.getImageData(this.proof.proofGuid, page.pageGuid)
                                .map(image => { // this is the rx map operator
                                    page.image = image;
                                    return page; 
                                })));

  // define stream to join tasks and pages into one stream and merge
  let combined$ = Observable.forkJoin(tasks$, pageImages$, 
                           (tasks, pageImages) => pageImages.map(page => { //array map operator
                              page.tasks = tasks;
                              return page;
                           }));
  return combined$; // return to caller
}

// execute and log full pages array
this.getFullPages(this.order.orderNum, this.pages)
    .subscribe(fullPages => console.log(fullPages));

note that up till you call subscribe, nothing executes, because you're just defining methods to handle and combine streams of data, and then once you do subscribe, everything executes at once and you get the final result in it's entirety when all have finished.

You might be wondering, well damn, that's a bit more code and way more complex, why do that? Well the reasons are 1. the more efficient execution and 2. this stream definition is fully REUSABLE. If I need to call it again, I can just call that function and subscribe to that stream definition and the fully defined pages array will be returned to you with only that single call. This may not be important to you in this case, but more often than not, it is extremely useful.

Sign up to request clarification or add additional context in comments.

1 Comment

Excelent explanation!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.