How to pass observable value to @Input() Angular 4

My appraoch and suggestion is to use BehaviorSubject for the following Reason This is from the doc:

One of the variants of Subjects is the BehaviorSubject, which has a notion of "the current value". It stores the latest value emitted to its consumers, and whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject.

I presume that the child component that the OP has declared would always need the last value emitted and hence I feel BehaviorSubject would be more suited to this.

Online-quote.component

@Component({
  selector: 'app-online-quote',
  templateUrl: './online-quote.component.html',
  styleUrls: ['./online-quote.component.css'],
  providers:  [DynamicFormService]
})
export class OnlineQuoteComponent implements OnInit {

  public answers$: BehaviorSubject<any>;

  constructor(private service: DynamicFormService) {
       this.answers$ = new BehaviorSubject<any>(null);

   }

  ngOnInit() {
    
    this.service.getAnswers("CAR00PR").subscribe(data => {
            this.answer$.next(data); // this makes sure that last stored value is always emitted;
         });
  }

}

In the html view,

<app-dynamic-form [answers]="answer$.asObservable()"></app-dynamic-form>

// this emits the subject as observable

Now you can subscribe the values in the child component. There is no change in how the answer is to be subscribed dynamic-form.component.ts

@Component({
  ...
})
export class DynamicFormComponent implements OnInit {
  @Input() answers: Observable<any>;

  ngOnInit() {
    this.answers.subscribe(val => {
      // deal with asynchronous Observable result
      this.form = this.qcs.toFormGroup(this.answers);
    })
}

Assume DynamicFormService.getAnswers('CAR00PR') is asynchronous(probably it is), using async Pipe to pass asynchronous result is on the right way, but you cannot expect to get the asynchronous result right now when DynamicFormComponent is created(at ngOnInit) because of Asynchonous. The result isn't ready yet when running your below line of code.

this.form = this.qcs.toFormGroup(this.answers);

There are several ways that can fix your problem.

1. listen to valueChange of @Input() answers at ngOnChanges lifehook.

ngOnChanges(changes) {
  if (changes.answers) {
    // deal with asynchronous Observable result
    this.form = this.qcs.toFormGroup(changes.answers.currentValue);
  }
}

2. pass Observable directly into DynamicFormComponent and subscribe to it to listen to it's result.

online-quote.component.html:

<app-dynamic-form [answers]="answers$"></app-dynamic-form>

dynamic-form.component.ts:

@Component({
  ...
})
export class DynamicFormComponent implements OnInit {
  @Input() answers: Observable<AnswerBase[]>;

  ngOnInit() {
    this.answers.subscribe(val => {
      // deal with asynchronous Observable result
      this.form = this.qcs.toFormGroup(this.answers);
    })
}

Passing an observable to the input is something to avoid, the reason is: What do you do with subscriptions on the previous observable when a new input is detected ? let's clarify, an observable can be subscribed, once you have a subscription this is your responsibility to either complete the observable or to unsubscribe. Generally speaking, this is even considered an anti-pattern to pass observables to functions which are not operators because you are doing imperative coding where observables are supposed to be consumed in a declarative way, passing one to a component is no exception.

If you really want to do so, you need to be very careful no to forget to unsubscribe to an observable once you did. To do so, you would either have to ensure the input is never changed or specifically complete any previous subscription to the overwritten input (well…right before it gets overwritten)

If you don't do so, you might end up with leaks and bugs are quite hard to find. I would therefore recommend the 2 following alternatives:

  • Either use a shared store service or "provide" it for the specific component, take a look at https://datorama.github.io/akita/ to see how. In this case, you are not using the inputs at all, you will only subscribe to the queries of the injected store service. This is a clean solution when several components needs to asynchronously write and read a shared source of data.

Or

  • Create an observable (BehaviourSubject, ReplaySubject or Subject) for the component which will emit when the input changes.
@Component({
  selector: 'myComponent',
  ...
})
export class DynamicFormComponent implements OnInit {

  // The Subject which will emit the input
  public myInput$ = new ReplaySubject();

  // The accessor which will "next" the value to the Subject each time the myInput value changes.
  @Input()
  set myInput(value){
     this.myInput$.next(value);
  }

}

And of course to let the input change, you will use the pipe async

<myComponent [myInput]="anObservable | async"></myComponent>

BONUS

And if you do not want to repeat yourself, I made a helper to observe any property within an object. Including properties decorated with Input() Here is an example Usage:

import { observeProperty$ } from "./observable-property";

@Component({
  selector: 'myComponent',
  ...
})
export class DynamicFormComponent implements OnInit {

@Input()
myInput: string;

// and here is an observable version of your property
myInput$ = observeProperty$(this, "myInput");

}

I had an almost identical use-case as OP and the proposed solution worked for me too.

For simplicity sake, I figured out a different solution that worked in my case and seems a little simpler. I applied the async pipe earlier on in the template as part of the *ngIf structural directive and used variables to be able to pass through the already evaluated value through to the child component.

<div *ngIf="answers$ | async as answers">
    <app-dynamic-form [answers]="answers"></app-dynamic-form>
</div>