如何将可观察值传递给@Input() Angular 4

69

我是Angular的新手,目前遇到了这样一种情况:我有一个服务 getAnswers():Observable<AnswerBase<any>[]>,以及两个相关的组件:

  • online-quote
  • dynamic-form

online-quote 组件在其 ngOnInit() 方法中调用了服务 getAnswers():Observable<AnswerBase<any>[]>,并将结果传递给了组件 dynamic-form

为了说明这种情况,以下是我的两个组件的代码:

online-quote.component.html:

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

在线报价组件.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);

  }

我的问题是,如果getAnswers():Observable<AnswerBase<any>[]>的结果信息是一个observable,那么从在线报价传递信息到动态表单的正确方法是什么。我已经尝试了很多种方式,但都没有成功。我希望有人能帮助我。非常感谢!

5个回答

85

假设 DynamicFormService.getAnswers('CAR00PR')异步的(很可能如此),使用async Pipe 传递异步结果是正确的方式,但是因为异步,你不能期望在DynamicFormComponent创建时(在ngOnInit中)立即获得异步结果。当运行下面的代码时,结果还没有准备好。

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

有几种方法可以解决您的问题。

1. 在 ngOnChanges 生命周期钩子中监听 @Input() answers 的 valueChange 事件。

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

2. 直接将Observable传入 DynamicFormComponent 并订阅其结果以便监听。

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

3
我真的很喜欢你提到了两种处理情况的可行方法。值得一提的是,在示例中并不是一个问题,但“subscribe”在组件代码中没有取消订阅机制的处理。再次强调,这里没有问题(除了不是问题的本质),因为我们知道原始的observable来源于一个http调用,该调用完成后就会结束。但对于其他observables,这可能会迅速成为一个有问题的内存泄漏。 - PigBoT
太棒了。在ngOninit中使用formbuilder.Group会产生零散的结果。有时可以运行,有时不行。ngOnChanges发生在ngOninit之前,这可能很重要,但关键是ngOnInit只会发生一次。我无法解释为什么我的子事件有时更新@Input值。 - greg

29

我有一个和楼主几乎完全相同的用例,所提出的解决方案对我也有效。

为了简单起见,我想出了一种不同的解决方案,在我的情况下似乎更简单。我在模板中的*ngIf结构指令中较早地应用了异步管道,并使用变量将已经评估过的值传递到子组件。

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

1
看起来不错,但是你如何在 app-dynamic-form 中处理它呢? - foo-baar
1
在 app-dynamic-form 中,它不再是一个 Observable,而是一个 AnswerBase<any>[]。我相信 OP 不必更改任何代码。 - rofer
2
如果他只是处理原始数据对象,他可能不需要进一步操作,这使得这个方案可行。然而,他正在将答案数据转换为一个表单组,因此,在已接受的答案中提到的ngOnChanges很好地补充了这个解决方案。 - PigBoT
运行得非常好,您可以在此处查看实现: https://angular-ivy-z9shje.stackblitz.io - manfall19
2
使用 ng-container 替代外部 div。 - ISONecroMAn
我相信在这种情况下我使用了<div/>,因为需要应用样式,但为了简洁起见,我将其删除了。但你绝对是正确的,<ng-container/>也可以工作 :) - Jonathan Kretzmer

11

我的方法和建议是使用BehaviorSubject来实现下面的目标。

原因如下,这是从文档中得到的:

Subjects的变体之一是BehaviorSubject,它具有“当前值”的概念。它存储了向其消费者发送的最新值,并且每当一个新的观察者订阅时,它将立即从BehaviorSubject接收到“当前值”。

我认为,OP声明的子组件总是需要最后一个发出的值,因此我认为BehaviorSubject更适合这种情况。

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

}

在HTML视图中,
<app-dynamic-form [answers]="answer$.asObservable()"></app-dynamic-form>

// 这会以observable形式发出主题

现在您可以订阅子组件中的值。订阅答案的方式没有改变。 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);
    })
}

7
避免将observable传递给输入,原因是:当检测到新的输入时,如何处理先前observable上的订阅?让我们澄清一下,一个observable可以被订阅,一旦你有了一个订阅,你就有责任完成observable或取消订阅。一般来说,将observable传递给不是操作符的函数甚至被认为是反模式,因为你正在使用命令式编程方式,而observable应该以声明式方式消耗,将其传递给组件也不例外。
如果您真的想这样做,您需要非常小心,不要忘记一旦完成订阅就取消observable的订阅。为此,您必须确保输入永远不会更改,或者在覆盖输入之前明确完成任何先前的订阅。
如果您不这样做,可能会出现泄漏,而且很难找到错误。因此,我建议使用以下2个替代方案:
  • 可以使用共享存储服务或者为特定组件“提供”它,查看https://datorama.github.io/akita/了解详情。在这种情况下,您根本不使用输入,只需订阅注入的存储服务的查询即可。当多个组件需要异步地写入和读取共享数据源时,这是一种清晰的解决方案。

或者

  • 为组件创建一个可观察对象(BehaviorSubject、ReplaySubject 或 Subject),当输入发生变化时会发出信号。
@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);
  }

}

当然,为了让输入发生变化,您需要使用管道异步。
<myComponent [myInput]="anObservable | async"></myComponent>

奖励

如果您不想重复自己,我制作了一个帮助程序来观察对象中的任何属性,包括使用Input()修饰的属性。 这里有一个例子 用法:

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

}

3
我遇到了同样的问题,并且创建了一个小型库来提供ObservableInput装饰器以帮助解决这个问题:https://www.npmjs.com/package/ngx-observable-input。针对此情况的示例代码如下:

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

  ...
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接