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

73,184

Solution 1

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

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

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

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

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>

Share:
73,184
AlejoDev
Author by

AlejoDev

I'm funny to java web development

Updated on July 09, 2022

Comments

  • AlejoDev
    AlejoDev almost 2 years

    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!

  • foo-baar
    foo-baar about 4 years
    Look good, but then how do you handle it inside app-dynamic-form ?
  • rofer
    rofer over 3 years
    Inside app-dynamic-form it's no longer an Observable, just an AnswerBase<any>[]. I believe the OP wouldn't have to change any code in there.
  • PigBoT
    PigBoT over 3 years
    I really like that you covered two viable ways of handling the situation. One thing worth mentioning, although not an issue based on the example, is the 'subscribe' is handled within the the component code without an unsubscribe mechanism. Again, not an issue here (besides not being the point of the question), where we know the original observable source is from an http call, which completes after the call. For other observables, this can quickly become a problematic memory leak.
  • PigBoT
    PigBoT over 3 years
    If he was just dealing with the raw data object, he probably would not need to do anything further, making this a viable solution. However, he is taking the answer data and converting it to a form group, so the ngOnChanges, mentioned in the accepted answer, would supplement this solution nicely.
  • manfall19
    manfall19 over 2 years
    Works as a charm, you can see an implementation here: angular-ivy-z9shje.stackblitz.io
  • ISONecroMAn
    ISONecroMAn about 2 years
    Use an ng-container instead of external div.
  • Jonathan Kretzmer
    Jonathan Kretzmer about 2 years
    I believe that I had used a <div/> in this case due to the need to apply styling, which I removed for the succinct example. But you're absolutely right that an <ng-container/> would have worked :)