Subscribing to Observable Input

I'm really not a fan of passing an Observable as an @Input itself, but I'm a big fan of turning a standard @Input() into an Observable. This is a much more flexible approach and makes it easier to keep a cleaner boundary between your component and the outside. As soon as you start subscribing to observables passed in from the parent component you open up a whole lot of unnecessary complexity.

This is the best way I've found to achieve this, and I'm increasingly using RxJS and seeing the power of what can be done. It's a little klunky, but I'm really holding out for better framework support for something like this in future.

// 'floating' input parameter, with default value of 'false'
@Input('floating') set floating(floating: boolean) { this.floating$.next(floating); };
private floating$ = new BehaviorSubject<boolean>(false);

Then in the parent component you can pass in a boolean:

<app-magic-box [floating]="true"></app-magic-box>
<app-magic-box [floating]="shouldFloatMagicBox"></app-magic-box>

If what you're sending in is stored 'outside' as an Observable, then by all means send it in using the async pipe. This leaves the subscribing and cleanup for this particular property up to the parent component (and the framework) instead of trying to handle it inside the magic box component.

<app-magic-box [floating]="float$ | async"></app-magic-box>

Inside your magic box component you can now use this observable however you want. Note that it has a default of false, so if we never set floating it still can be used.

 public showRabbit$: Observable<boolean>;

 ngOnInit()
 {
     // (only run this initialization once)

     // subscribe directly if you wish, but keep a reference to dispose in ngOnDestroy
     this.floating$.subscribe(floating => ...);

     // we can also create a new observable based on our input parameters
     // with whatever RxJS you want. if a new input value is set it will 
     // propagate through all the pipes

     // show a rabbit when not floating (why not!)
     this.showRabbit$ = this.floating$.pipe(map(floating => floating == false));
 }

Now you have another observable inside your magic box that you can use in the template. You'll find the 'async' pipe propagating around, but that's fine.

<img *ngIf="showRabbit$ | async" src="./rabbit.png"/>

Important note: If you want to use this as a @HostBinding you'll have to do something like this:

@HostBinding("attr.floating")
get attr_floating() { return this.floating$.value; }

If you really have a problem about accessing value on a BehaviorSubject then give me a better solution ;-) In the context of what I'm doing here I have no issue with it.

I'm really hoping in future the framework will have something like an @InputObservable that does something similar a little more magically than we can achieve today.

There's a long standing open issue on Github - but I must warn you it's not for the faint of heart!


You should implement OnChanges and subscribe to the input when it changes.

export class MyComponent implements OnChanges {
  @Input() results: Observable<string[]>;

  constructor() { }

  ngOnChanges(changes){
      if(changes["results"] && this.results){
          this.results.subscribe(value => ...);
      }
  } 
}

This will allow you to subscribe to the Observable once it is available and re-subscribe to it anytime that the Observable reference changes. You may need to consider unsubscribing from old instance depending on your use-case.

Tags:

Angular