-1

How do I get the result of setUserCourses in UserCourseService to bubble up to DashboardBodyHomeCourseModalComponent? At the moment, result returns null.

Also, I am currently passing in an array called courseSelectedOptions which executes multiple HTTP calls to my API. At the end of the last API call, I want to bubble up a HTTP post response to my component.

UserCourseService

setUserCourses(userId: string, courseSelectedOptions: number[]): Promise<unknown | void> {
return this.firebaseService.generateToken().then((token: string) => {
  this.logger.debug(`Token generated for setUserCourses: ${token}`);
  return observableFrom(courseSelectedOptions)
    .pipe(
      concatMap((courseSelectedOption) =>
        this.httpClient.post(
          `${environment.apiBasePath}/userCourses`,
          {
            user_id: userId,
            course_id: courseSelectedOption,
          },
          { headers: { authorization: `Bearer ${token}` } }
        )
      )
    )
    .subscribe(
      (response) => this.logger.debug(`setUserCourses response: ${JSON.stringify(response)}`),
      (error) => this.logger.debug(`setUserCourses error: ${JSON.stringify(error)}`),
      () => this.logger.debug('setUserCourses complete')
    );
});
}

DashboardBodyHomeFacade

setUserCourses(userId: string, courseSelectedOptions: number[]): Promise<unknown | void> {
    return this.userCourseService.setUserCourses(userId, courseSelectedOptions);
}

DashboardBodyHomeCourseModalComponent

this.dashboardBodyHomeFacade
.setUserCourses(this.user.id, this.courseSelectedOptions)
.then((result: unknown) => {
  if (result) {
    this.toastrService.success('Your courses have been selected.', 'Great!');
    this.bsModalRef.hide();
  } else {
    this.toastrService.warning('Something has gone wrong. Please try again.', 'Oops!');
  }
})
.catch(() => {
  this.toastrService.warning('Something has gone wrong. Please try again.', 'Oops!');
});
Raghav Garg
  • 3,601
  • 2
  • 23
  • 32
methuselah
  • 12,766
  • 47
  • 165
  • 315
  • you need to return a value from your observable, something like given in this link https://stackoverflow.com/questions/38291783/how-to-return-value-from-function-which-has-observable-subscription-inside, – Raghav Garg Oct 25 '20 at 13:41
  • Not really, I don't understand how to implement it in my current architecture, do I return the value in the facade or service or component? – methuselah Oct 25 '20 at 13:47
  • 1
    In service, you need to convert your observable to a promise when returning, so that further facade and component can use the method `.then` on it to resolve the promise and get the value – Raghav Garg Oct 25 '20 at 13:49
  • I've updated the question to make it more clearer, there is the added complexity of executing multiple HTTP calls in series. – methuselah Oct 25 '20 at 13:58

1 Answers1

1

This is a classic case of mixing observables and promises. I'd recommend you to convert the promise to a observable using RxJS from function. After that you could map to the HTTP request using a higher order mapping operator (switchMap here), return it from the service and subscribe in the component.

Service

setUserCourses(userId: string, courseSelectedOptions: number[]): Observable<any> { // <-- return the observable
  return from(this.firebaseService.generateToken()).pipe(
    switchMap(token => {
      this.logger.debug(`Token generated for setUserCourses: ${token}`);
      return this.httpClient.post(
        `${environment.apiBasePath}/userCourses`,
        {
          user_id: userId,
          course_id: courseSelectedOptions,
        },
        { headers: { authorization: `Bearer ${token}` } }
      )
    }),
    tap(
      (response) => this.logger.debug(`setUserCourses response: ${JSON.stringify(response)}`),
      (error) => this.logger.debug(`setUserCourses error: ${JSON.stringify(error)}`),
      () => this.logger.debug('setUserCourses complete')
    )
  );
}

Component

this.dashboardBodyHomeFacade.setUserCourses(this.user.id, this.courseSelectedOptions).subscribe(
  result => {
    if (result) {
      this.toastrService.success('Your courses have been selected.', 'Great!');
      this.bsModalRef.hide();
    } else {
      this.toastrService.warning('Something has gone wrong. Please try again.', 'Oops!');
    }
  },
  error => this.toastrService.warning('Something has gone wrong. Please try again.', 'Oops!')
);

Update: multiple requests for each item in courseSelectedOptions

You could use RxJS forkJoin function along with Array#map to trigger multiple parallel HTTP requests for each item in courseSelectedOptions. Try the following

setUserCourses(userId: string, courseSelectedOptions: number[]): Observable<any> { // <-- return the observable
  return from(this.firebaseService.generateToken()).pipe(
    switchMap(token => {
      this.logger.debug(`Token generated for setUserCourses: ${token}`);
      return forkJoin(courseSelectedOptions.map(courseSelectedOption => // <-- trigger parallel requests for each item in `courseSelectedOptions`
        this.httpClient.post(
          `${environment.apiBasePath}/userCourses`,
          {
            user_id: userId,
            course_id: courseSelectedOption,
          },
          { headers: { authorization: `Bearer ${token}` } }
        )
      ))
    }),
    tap(
      (response) => this.logger.debug(`setUserCourses response: ${JSON.stringify(response)}`),
      (error) => this.logger.debug(`setUserCourses error: ${JSON.stringify(error)}`),
      () => this.logger.debug('setUserCourses complete')
    )
  );
}
ruth
  • 29,535
  • 4
  • 30
  • 57
  • 1
    I have a doubt regarding "passing over an observable instead of a promise from service", why do we want to do that? – Raghav Garg Oct 25 '20 at 13:51
  • Observable's memory footprint is more than promise and if the data is coming from the HTTP request and is not going to change, it's better to use promise because observable will keep on watching for changes which is of no use on an HTTP req – Raghav Garg Oct 25 '20 at 13:54
  • 1
    There are multiple reasons. For one, the promise is the starting point here. It's only used to fetch the token and we move forward with an observable. Moreover observable provides more features to control the flow of data compared to a promise. – ruth Oct 25 '20 at 13:54
  • There is one issue however, `courseSelectedOption` is an array and with this approach, it tries to insert all of the values at once instead of one at a time. – methuselah Oct 25 '20 at 13:55
  • @methuselah: So you need multiple HTTP requests for each element in the `courseSelectedOptions` array? – ruth Oct 25 '20 at 13:56
  • @MichaelD, yes. In my initial example, I think `concatMap((courseSelectedOption)` handled this. – methuselah Oct 25 '20 at 13:57
  • I've updated the question to make it more clearer, as there is the added complexity of executing multiple HTTP calls in series. – methuselah Oct 25 '20 at 13:58
  • 1
    Observable establish common interface with other things in Angular and allow you to write app reactively. Memory footprint in this case is not than promise if you remember to unsubscribe. Regarding XHR request, Observables allow cancelling request e.g when you leave component for which data is being fetched.Moreover, you can compose one lazy flow with other Observable and you don't have to call XHR eagerly (as promises work). – Marcin Milewicz Oct 25 '20 at 13:58
  • @methuselah: You could use RxJS `forkJoin` for it. I've updated the answer. – ruth Oct 25 '20 at 14:00
  • @RaghavGarg: In light of the modifications, it's easier to combine multiple observables to a single subscription rather than to handle multiple subscriptions to boil down to a promise. And as Marcin noted, subscription in the context of Angular HttpClientModule allows to cancel HTTP requests inflight. – ruth Oct 25 '20 at 14:03
  • @MarcinMilewicz and MichealD Thanks for the explanation, it makes sense. Now, I have to go and read more about observables. :P. – Raghav Garg Oct 25 '20 at 14:04