How to pass observable value to @Input() Angular 4
Asked Answered
W

5

69

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!

Weaponless answered 5/4, 2018 at 0:23 Comment(0)
G
86

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() {
    // assumes we do not need to unsubscribe from answers
    // otherwise include unsubscribe logic (in ngOnDestroy)
    this.answers.subscribe(val => {
      // deal with asynchronous Observable result
      this.form = this.qcs.toFormGroup(this.answers);
    })
  }

  // ...
}
Glacialist answered 5/4, 2018 at 1:1 Comment(2)
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.Hie
Brilliant. Having the formbuilder.Group in ngOninit was producing sporadic results. Would work sometimes, and not sometimes. ngOnChanges occurs before ngOninit, which may be important, but the key is ngOnInit happens only once. I can not explain why my child event was updating the @Input value 'sometimes'Varied
A
30

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>
Alsoran answered 31/7, 2019 at 8:30 Comment(6)
Look good, but then how do you handle it inside app-dynamic-form ?Gigantic
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.Derek
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.Hie
Works as a charm, you can see an implementation here: angular-ivy-z9shje.stackblitz.ioMonaural
Use an ng-container instead of external div.Trista
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 :)Alsoran
F
11

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);
    })
}
Fowling answered 7/1, 2021 at 15:0 Comment(0)
R
8

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

}

[EDIT] Since Ng17.1, you can use Signal Inputs i.e. the input can now be a Signal. It's easy to natively convert them into observable.


@Component({
  selector: 'myComponent',
  ...
})
export class MyComponent {

// this is your id input, (no more @Input() decorator)
public id: InputSignal<string> = input.required<string>();

// and here is an observable version of your property
public insuredId$: Observable<string> = toObservable(this.insuredId);

}

You have 3 ways to instance your InputSignal

  • input(); // no default value, the signal can implicitly be undefined
  • input("defaultValue"); // using a default value (no undefined)
  • input.required(); // no implicit undefined, you must however provide an input in the component's or Angular's compiler will complain.
Rosenkranz answered 12/1, 2022 at 13:57 Comment(0)
T
3

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[]>;

  ...
}
Tracitracie answered 30/11, 2019 at 14:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.