Thursday, September 27, 2018

Angular Noob: An Observable On An Observable Observing a Promise

With the reusability and extensibility of modern Web components, I do not look back on the days of jQuery with much fondness.  However, I haven't paid much attention to Angular since Angular 1.  Since its syntax didn't really appeal to me, I opted to learn Polymer instead.  Well now, given a new opportunity, I am diving into a much more modern Angular and TypeScript.  Unfortunately, I am finding that a lot of articles people write on Angular, when you're diving into a well-established code base, are about as dense as reading toward the end of a book on quantum mechanics.  It's English alright, but the jargon is applied thickly.  (And this is coming from someone who has even impressed some of Google's Tensorflow engineers with their machine learning skillz.)

The problem at hand is fairly straightforward.  We want to notify something in the UI upon the outcome of a RESTful request we make to an external resource so that it can display useful information to the user.  We call http.get(), which returns an Observable of type Response (Observable<Response>).  Upon the outcome of the Observable (basically the one event this particular instance fires), we will run either onResponse() or onError().

To describe this in code, imagine the following:

Main App TypeScript File:

ngOnInit() {
  this.dataService.loadFromAPIOrOtherSite();
  // handle routing, and whatever else you can imagine happening here
}


Data Service TypeScript file:


loadFromAPIOrOtherSite() {
  this.dataLoader.loadData().subscribe(
    user => this.onResponse(user),
    error => this.onError(error)
  );
}

Data Loader Service TypeScript file:

loadData() {
  return this.http.get(url)
    .map(response => this.transposeData(response.json()))

    .catch(error => Observable.throw(error));

The way this works is that once the page loads, the data will be fetched.  The obvious problem here is that the main page never gets informed as to the status of the data fetch; as such, the user is not notified when the server fails to respond properly.  Now, theoretically, you could inform the data service about the UI you are looking to manipulate, but I think it makes more sense for the page to deal with its own issues, rather than anything else.

It becomes apparent that what I need to do is get the loadFromAPIOrOtherSite() function to in fact be an Observable itself.  The loadFromAPIOrOtherSite() function utilizes an Observable, so of course the loadData() function returns an Observable that resolves into either the successful answer or an error message.  Unfortunately, a lot of the pedagogy on this topic informs you to use some of the chaining or aggregation functions found in the RxJs library, such as map(), which is overkill for a single GET request.  I don't have a whole array of things to process, nor do I care to append the output of one Observable directly to another Observable.  And, even if there was an array of things to process, it's unclear to me how I could allow the side processes to complete while still returning the request and its status to the main page controller.  I also don't want either of the data services manipulating the DOM directly in order to show the user an error message -- I want the main page controller to handle this.

After enough searching around on Stack Overflow, I finally came across this answer that shows how to nest Observables in a plain fashion, without anything fancy.  It nests an Observable in an Observable by observing the Subscription coming out of the subscribe() function.


Applying This To the Code


There's a little bit of extra logic in here to deal with what happens when the loadFromAPIOrOtherSite() call finishes before or after ngAfterViewInit().  On one hand, you might try to manipulate DOM elements that aren't rendered yet, leading to an undefined mess.  On the other hand, the view might finish rendering before the data load has finished.

Main App TypeScript File:

// You'll want this to deal with timing of the completion of your Observable

import { AfterViewInit } from '@angular/core';

ngOnInit() {
  this.dataService.loadFromAPIOrOtherSite().subscribe(
    data => {
      // happy path
      this.done = true;
      doSomethingOnUI();
    },
    error => {
      // unhappy path
      this.done = true;
      doSomethingOnUI();
    }
  )};
}

ngAfterViewInit() {
  this.elem = document.querySelector('#elem');
  doSomethingOnUI();
}

doSomethingOnUI() {
  if (this.elem && this.done) {
    // do something with this.elem
  }
}

Data Service TypeScript file:


import { Observable } from '@rxjs/Observable';

import { Observer } from '@rxjs/Observer';

loadFromAPIOrOtherSite() {
  return Observable.create((observer: Observer<any>) => {
    this.dataLoader.loadData().subscribe(
      data => {
        observer.next(this.onResponse(data));
        observer.complete();
      },
      error => {
        observer.next(this.onError(error));
        observer.complete();
      }
    );
  )};
}

Now, it's helpful when 
this.onResponse() and this.onError() return something (even as simple as a string or integer), because observer.next() propagates that return value as an "observation" to the subscriber to loadFromAPIOrOtherSite().  And, with observer.complete(), it will be the last thing that subscription will ever receive.

Nesting This Even Further: Moar Nesting!


It's possible that the previous example doesn't go as far as you need.  What if you want to do something else, like check for incomplete data inside this.onResponse() and augment it with additional data, or show an error to the user if it can't be augmented in the necessary way?  And on top of that, how about that this extra data collection function returns a Promise rather than an Observable?  Let's build upon the previous idea and make even more wrappers.

Note that the Data Service TypeScript file now has a subscription to onResponse() as well, not just loadData():

loadFromAPIOrOtherSite() {
  return Observable.create((observer: Observer<any>) => {
    this.dataLoader.loadData().subscribe(
      data => {
        this.onResponse(data).subscribe(
          augmentedData => {
            observer.next(augmentedData);
            observer.complete();
          }
       // etc...

We must also modify onResponse() to return an Observable itself, and not just a basic literal or some JSON object.  You'll notice this follows a similar pattern to before, along with handling a lot of possible unhappy paths:

onResponse(data) {
  // used to just "return 42;" or something simple like that
  return Observable.create((observer: Observer<any>) => {
    if (!isTotallyUnsuitable(data)) {
      let moarData = Observable.fromPromise(this.promiseService.promiseReturner());
      moarData.subscribe((data) => {
        if (cantAugment(data)) {
         observer.next(() => {return "Failure to augment the data"});
         observer.complete();
        }
        // augment the data here (happy path)
        observer.next(data);
        observer.complete();
      }
    } else {
      observer.next(() => {return "Failure to get good data at all"});
      observer.complete();
    }
  });
}

Epilogue


Now, if you know how to do such complex Observable nesting with map(), concatMap(), or forkjoin(), you're welcome to let the world know in the comments below!  And be sure to upvote the Stack Overflow post below if you liked this article!

Sources:



  • https://stackoverflow.com/questions/49630371/return-a-observable-from-a-subscription-with-rxjs/49631711#49631711
  • https://alligator.io/rxjs/simple-error-handling/