BeetleJuice
BeetleJuice

Reputation: 40896

Angular 4 and RxJS 5: Observable.concat() behaving unexpectedly

My Angular 4 / TypeScript 2.3 service has a function build() that errors if a class property isn't initialized. I'm trying to build a safer version -- safeBuild() -- that will return an Observable that will wait and listen for the property to be initialized before trying to call build()

export class BuildService {
  
  renderer:Renderer2; // must be set for build() below to work
  
  // emits the new Renderer2 when renderer is set
  private rendererSet$:BehaviorSubject<Renderer2> = new BehaviorSubject(null);

  /** Set renderer, and notify any listener */
  setRenderer(renderer:Renderer2){
    this.renderer = renderer;
    this.rendererSet$.next(renderer);
  }

  /** Returns a new DOM element. Requires renderer to be set */
  build(elemTag:string){
    // if renderer is not set, we can't proceed
    // why is this error thrown when safeBuild() is called?
    if (!this.renderer) 
      throw new Error('Renderer must be set before build() is run');

    return this.renderer.createElement(elemTag);
  }

  /**
   * A safe version of build(). Will wait until renderer is set
   * before attempting to call build (Asynchronous)
   */
  safeBuild(elemTag:string):Observable<any> {
    // inform user that renderer should be set
    // this warning is printed to the console as expected
    if (!this.renderer) 
      console.warn('The build will be delayed until setRenderer() is called');

    // Listen to rendererSet$, filter out the null output, and call build()
    // only once the renderer is set. Why does the error still get thrown?
    return Observable.concat(
      this.rendererSet$.filter(e=>!!e).take(1),
      Observable.of(this.build(elemTag))
    )
  }
}

I try to build like this (from another service):

this.buildService.safeBuild(elemTag).subscribe(...)

In the console I see:

Warn: The build will be delayed until setRenderer() is called

Error: Renderer must be set before build() is run

I expected the warning, but then nothing to happen until another part of my app calls setRenderer(). At that point, the code in subscribe() would run.

Why do I see the error?

Upvotes: 2

Views: 1067

Answers (2)

cartant
cartant

Reputation: 58400

The problem is that this.build(elemTag) is called when composing the concat observable - not when the concatenation is performed.

You could solve the problem using defer:

import 'rxjs/add/observable/defer';

...
return Observable.concat(
  this.rendererSet$.filter(e => !!e).take(1),
  Observable.defer(() => Observable.of(this.build(elemTag)))
);

Or, as pointed out in the comments, using map:

return this.rendererSet$
  .filter(e => !!e)
  .take(1)
  .map(() => this.build(elemTag));

Upvotes: 3

Bunyamin Coskuner
Bunyamin Coskuner

Reputation: 8859

It is because, you create an Observable of whatever this.build function returns. Since, you haven't set the renderer yet, the line below throws an error. Make sure you call setRenderer function first

if (!this.renderer) 
    throw new Error('Renderer must be set before build() is run');

You should be able to solve this by returning an Observable as follows

return Observable.concat(
  this.rendererSet$.filter(e=>!!e).take(1),
  this.rendererSet$.asObservable().map(() => this.build(elemTag)) // this line will execute when there is a new value set to rendererSet
)

Upvotes: -1

Related Questions