【问题标题】:Angular @Output with callback带有回调的 Angular @Output
【发布时间】:2019-10-30 19:02:03
【问题描述】:

是否可以使用@Output 进行回调?

我有一个FormComponent,它检查有效性,并在提交时禁用提交按钮。现在我想在提交完成后重新启用提交按钮。

@Component({
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      ...
    </form>
  `
})
class FormComponent {
  form: FormGroup = ...;

  isSubmitting = false;

  @Output()
  submitted = new EventEmitter<MyData>()

  onSubmit() {
    if(this.form.invalid || this.isSubmitting) {
      return;
    }

    this.isSubmitting = true;

    this.submitted.emit(this.form.value);
    // Here I'd like to listen for the result of the parent component
    // something like this...
    // this.submitted.emit(...).subscribe(res => this.isSubmitting = false);
  }
}
@Component({
  template: `
    <my-form (submitted)="onSubmitted($event)"></my-form>
  `
})
class ParentComponent {
  constructor(private service: MyService) { }

  onSubmitted(event: MyData) {
    this.service.doSomething(event).pipe(
      tap(res => console.log("service res", res)
    );
    // basically I'd like to `return` this `Observable`,
    // so the `FormComponent` can listen for the completion
  }
}

我知道,我可以在 FormComponent 中使用 @Input() 并执行以下操作:

@Input()
set submitted(val: boolean) {
  this.isSubmitted = val;
}

但我想知道是否有更简单/更好的解决方案,因为isSubmitted 应该是FormComponent 的内部属性,它应该由组件本身而不是其父级管理。

【问题讨论】:

  • 我认为在服务中这样做更优雅。你可以在任何你想要的地方注入服务。我会在服务中使用isSubmitted$: Subject&lt;boolean&gt;
  • 你的意思是某种FormService?那么它必须由ParentComponent 提供(因为同一个表单可以在同一个屏幕上多次显示)。然后FormComponent 注入它并观察isSubmitted$ 流。是的,这也可能有效。但我希望有更简单的东西......
  • 更短的版本是在父级中使用@ViewChild(FormComponent) form,然后使用form.isSubmitting = false。有效地完成相同的工作。

标签: angular callback rxjs eventemitter


【解决方案1】:

另一个使用模板变量的解决方案:

@Component({
  template: `
    <my-form (submitted)="onSubmit($event, form)" #form></my-form>
  `
})
class ParentComponent {
  constructor(private service: MyService) { }

  onSubmit(event: MyData, form: FormComponent) {
    // don't forget to unsubscribe
    this.service.doSomething(event).pipe(
      finalize(() => {
        form.setSubmitting(false);
      })
    ).subscribe();
  }
}
class FormComponent {
  form: FormGroup = ...;

  isSubmitting = false;

  @Output()
  submitted = new EventEmitter<MyData>()

  constructor(private cdr: ChangeDetectorRef) { }

  setSubmitting(val: boolean) {
    this.isSubmitting = val;
    this.cdr.markForCheck();
  }

  onSubmit() {
    if (!this.form.valid || this.isSubmitting) {
      return;
    }

    this.isSubmitting = true;

    this.submitted.emit(this.form.value);
  }
}

【讨论】:

    【解决方案2】:
     onSubmit() {
        this.isSubmitting = true;
        this.submitHandler(this.form.value).subscribe(res => {
          this.isSubmitting = false;
          this.cdr.markForCheck();
        });
      }
    

    在上面的示例代码中,函数onSubmit() 不是一个无状态函数,它依赖于一个外部 处理程序。从测试的角度使函数本身不可预测。如果失败(如果失败),您将不知道在哪里、为什么或如何。回调也会在组件被销毁之后执行

    禁用的问题是组件消费者的外部状态。所以我只是把它变成一个输入绑定(就像这里的其他答案一样)。这使得组件更并且更容易测试。

    @Component({
      template: `<form [formGroup]="form" (ngSubmit)="form.valid && enabled && onSubmit()"</form>`
    })
    class FormComponent {
      form: FormGroup = ...;
    
      @Input()
      enabled = true;
    
      @Output()
      submitted = new EventEmitter<MyData>()
    
      onSubmit() {
        // I prefer to do my blocking in the template
        this.submitted.emit(this.form.value);
      }
    }
    

    这里的关键区别是我使用下面的enabled$ | async 来支持OnPush 更改检测。由于组件的状态是异步变化的。

    @Component({
      template: `<my-form [enabled]="enabled$ | async" (submitted)="onSubmitted($event)"></my-form>`
    })
    class ParentComponent {
      public enabled$: BehaviorSubject<boolean> = new BehaviorSubject(true);
    
      constructor(private service: MyService) { }
    
      onSubmitted(event: MyData) {
        this.enabled$.next(false);
        this.service.doSomething(event).pipe(
          tap(res => this.enabled$.next(true)
        ).subscribe(res => console.log(res));
      }
    }
    

    【讨论】:

    • 在我的实际实现中我会使用this.submitHandler(this.form.value).pipe(takeUntil(this.destroyed$),finalize(this.submitting = false; this.cdr.markForCheck();}).subscribe();。 (这就是为什么我把评论“不要忘记取消订阅销毁”;-))。虽然销毁后不会被执行。我不明白你对测试的看法。 FormComponent 有一个特殊的合同,只有在该合同得到履行的情况下才能工作。因此,如果您不提供永远不会完成的submitHandlersubmitHandler,那么您就没有履行组件的合同。还是我错过了什么?
    • @BenjaminM 我所说的测试是指函数的内部状态由外部回调控制。因此,虽然单元测试可以通过,但代码仍然可能失败,因为函数不是纯函数。
    【解决方案3】:

    我找到了另一种方法:通过将处理函数传递为@Input

    class FormComponent {
      form: FormGroup = ...;
    
      isSubmitting = false;
    
      @Input()
      submitHandler: (value: MyData) => Observable<any>;
    
      constructor(private cdr: ChangeDetectorRef) { }
    
      onSubmit() {
        if (!this.form.valid || this.isSubmitting) {
          return;
        }
    
        this.isSubmitting = true;
    
        // don't forget to unsubscribe on destroy
        this.submitHandler(this.form.value).subscribe(res => {
          this.isSubmitting = false;
          this.cdr.markForCheck();
        });
      }
    }
    
    @Component({
      template: `
        <my-form [submitHandler]="submitHandler"></my-form>
      `
    })
    class ParentComponent {
      constructor(private service: MyService) { }
    
      submitHandler = (formValue: MyData): Observable<any> => {
        return this.service.doSomething(event);
      };
    }
    

    这很容易使用并且效果很好。唯一“不好”的事情是,感觉就像我在滥用 @Input 来做一些非设计的事情。

    【讨论】:

      【解决方案4】:

      我不知道这样的回调。充其量你可以做一些@Input接线。

      parent.component.html

      &lt;my-form (submitted)="formSubmit($event)" [disableButton]="disableButton"&gt;&lt;/my-form&gt;

      parent.component.ts

      disableButton: boolean = false;
      
      formSubmit(myForm) {
       this.disableButton = true; --> disable it here as soon as form submitted.
      
       this.service.doSomething(event).pipe(
        tap(res => {
        console.log("service res", res);
        this.disableButton = false; // --> enable it here when form submission complete
         }
       ));
      
      }
      

      child.component.ts

      @Input() disableButton: boolean

      child.component.html

      &lt;button [disabled]="disableButton?'':null"&gt;Submit&lt;/button&gt;

      所以一种方法是在这些线上实现。

      【讨论】:

      • 是的,这是我在问题中已经指出的一个解决方案,使用@Input() set submitted。但是在使用OnPush 时,您还必须在this.disableButton = false 之后使用this.cdr.markForCheck()
      • 糟糕!我错过了。是的,这是一种解决方案。我认为在我能想到的任何其他解决方案中都没有从父组件中逃脱。就像一种解决方案一样,使用主题并与表单数据一起将主题发送给父级。提交完成后,通过收到的主题启用禁用按钮。其他解决方案是查看您自己想到的子方法。
      【解决方案5】:

      您可以在父组件中设置 isSubmiting 并将其作为输入提供给子组件。在您的情况下,解决方案是在父组件中初始化 isSubmitting 并将其设置为 false。然后,当您从子组件发出一个值时,父回调集的第一行 isSubmitting 为 true。一旦 onSubmitted 的逻辑完成后,您可以再次将 isSubmitting 设置为 false。您在子组件中要做的就是接收 isSubmitted 作为输入并将其设置为输入类型 submit as bind attr [disabled]="isSubmitting"

      【讨论】:

      • 你能举个例子吗?
      猜你喜欢
      • 2019-11-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-12
      • 2014-10-28
      • 1970-01-01
      • 2015-04-29
      相关资源
      最近更新 更多