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

AngularTypescriptRxjsAngular2 ObservablesAngular2 Decorators

Angular Problem Overview


I am new to angular and I have the following situation which is I have a service getAnswers():Observable<AnswerBase<any>[]>and two components that are related to each other.

  • online-quote
  • dynamic-form

online-quote component calls the service getAnswers():Observable<AnswerBase<any>[]> in its ngOnInit() method and the result of this, is passed to the component dynamic-form.

To illustrate the situation this is the code of my two components:

online-quote.component.html:

 <div>
    <app-dynamic-form [answers]="(answers$ | async)"></app-dynamic-form>
</div>

online-quote.component.ts:

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

  public answers$: Observable<any[]>;

  constructor(private service: DynamicFormService) {
    
   }

  ngOnInit() {
    this.answers$=this.service.getAnswers("CAR00PR");
  }

}

dynamic-form.component.html:

<div *ngFor="let answer of answers">
 <app-question *ngIf="actualPage===1" [answer]="answer"></app-question>
</div>

dynamic-form.component.ts:

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css'],
  providers: [ AnswerControlService ]
})
export class DynamicFormComponent implements OnInit {
  @Input() answers: AnswerBase<any>[];
  
  constructor(private qcs: AnswerControlService, private service: DynamicFormService) {  }

  ngOnInit() {
    
    this.form = this.qcs.toFormGroup(this.answers);
    
  }

My question is what is the correct way to pass the information from online-quote to dynamic-form if the result information of the service getAnswers():Observable<AnswerBase<any>[]> is a observable.

I've tried it in many ways but it does not work. I would like someone to help me with this. Thank you very much!

Angular Solutions


Solution 1 - Angular

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);
    })
}

Solution 2 - Angular

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>

Solution 3 - Angular

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);
    })
}

Solution 4 - Angular

I had the same issue and I've created a small library that provides ObservableInput decorator to help with that: https://www.npmjs.com/package/ngx-observable-input. Example code for that case would be:

online-quote.component.html:

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

dynamic-form.component.ts:

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

  ...
}

Solution 5 - Angular

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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlejoDevView Question on Stackoverflow
Solution 1 - AngularPengyyView Answer on Stackoverflow
Solution 2 - AngularJonathan KretzmerView Answer on Stackoverflow
Solution 3 - Angularvijayakumarpsg587View Answer on Stackoverflow
Solution 4 - AngularFutharkView Answer on Stackoverflow
Solution 5 - AngularFlavien VolkenView Answer on Stackoverflow