Angular @Output with callback

Viewed 3581

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.

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

I am not aware of such callback. At best you can do is do some @Input wiring.

In parent.component.html

<my-form (submitted)="formSubmit($event)" [disableButton]="disableButton"></my-form>

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

}

In child.component.ts

@Input() disableButton: boolean

In child.component.html

<button [disabled]="disableButton?'':null">Submit</button>

So one way is to implement on these lines.

You can set isSubmiting in Parent component and provided it as Input to child component. In your case solution will be, initialize isSubmitting in parent component and set it to false. Then when from child component u emit a value on first line of parent callback set isSubmitting to true. Once when logic for onSubmitted is done u can set isSubmitting to false again. All u have to do in child component is to receive isSubmitted as input and set it to input type submit as bind attr [disabled]="isSubmitting"

I found another way: By passing a handler function as @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);
  };
}

That's easy to use and works pretty well. The only "bad" thing is, that it feels like I'm abusing @Input for something it wasn't designed.

And another solution using a template variable:

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