Search code examples
angularcallbackrxjseventemitter

Angular @Output with callback


Is it possible to have a callback with @Output?

I have a FormComponent which checks validity, and disables the submit button while submitting. Now I'd like to reenable the submit button, when submitting has finished.

@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
  }
}

I know, I could use an @Input() within FormComponent and do something like this:

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

But I'd like to know if there's a simpler / better solution, because isSubmitted should be an internal property of FormComponent, which should be managed by the component itself and not its parent.


Solution

  •  onSubmit() {
        this.isSubmitting = true;
        this.submitHandler(this.form.value).subscribe(res => {
          this.isSubmitting = false;
          this.cdr.markForCheck();
        });
      }
    

    In the above example code, the function onSubmit() is not a stateless function and depends upon an external handler. Making the function itself unpredictable from a testing perspective. When this fails (if it does) you won't know where, why or how. The callback also ricks being executed after the component has been destroyed.

    The problem of being disabled is an external state by the consumer of the component. So I would just make it an input binding (like the other answer here). This makes the component more dry and easier to test.

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

    The key difference here is that I use enabled$ | async below to support OnPush change detection. Since the state of the component changes asynchronously.

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