7

I have a dialog component, which executes two async functions on submit. My goal is to keep the dialog opened and show a loading state until both functions are finished. After it, I want to close the dialog.

My submit function, which is defined in parent component, looks like this:

 async submit() {
    await this.foo1();
    await this.foo2();
}

This function is passed as a prop to the dialog component:

<app-dialog @submit="submit" />

In my dialog component, on a button click, I try to do this:

async onClick() {
    await this.$emit('submit');
    this.closeDialog();
},

However, dialog is closed immediately instead of waiting for submit to be executed. What's the best way to accomplish this?

Coelacanth
  • 597
  • 1
  • 5
  • 18

2 Answers2

28

I managed to find a solution by passing a callback in dialog component:

submit() {
    this.$emit('submit', () => this.closeDialog)
},

then using @submit="submit" on the parent component, and defining 'submit' as such:

async submit(closeDialog) {
    await this.foo1();
    await this.foo2();
    closeDialog()
}

But there must be a better solution than this!

Avi Meltser
  • 409
  • 4
  • 11
Coelacanth
  • 597
  • 1
  • 5
  • 18
  • 1
    I think this is an elegant solution! – tony19 Mar 11 '21 at 13:52
  • 1
    Can't be more elegant than async/await! – Coelacanth Mar 11 '21 at 13:53
  • I really like this one, beause you could pass your arguments alongside in the emit, where https://stackoverflow.com/a/66585473/6434747 makes it hard to pass any arguments because then it would just execute the function – Nebulosar Nov 18 '21 at 08:48
  • 2
    Coming back several month later, I'd like to up-vote this one once again. – Alan Kersaudy Mar 28 '22 at 21:33
  • I think another solution is to make new method in the child that will close the dialog. Then, this method will be called from the parent exactly after the `foo1` and `foo2` are finished, but I think yours is more elegant and nice :) Thanks! – Vitomir Mar 31 '23 at 10:32
5

There's an alternative pattern for this kind of problem, namely passing a callback function as a prop.

On your dialog component:

props: {
  onSubmit: {
    type: Function,
    required: true // this is up to you
},

[...]

// in your methods
async onClick() {
  if (this.onSubmit && typeof this.onSubmit === 'function') {
    await this.onSubmit();
  }
  this.closeDialog();
}

and then, in your parent component:

<app-dialog :on-submit="submit" />

[...]

// in your methods:

async submit() {
  await this.foo1();
  await this.foo2()
}

Please keep some things in mind though

  1. It will be important where you handle your promises. For example, if you want to keep the modal open in case of error, you could do error handling in the modal component, or at least forward some error to it.

  2. It's worth exploring validation of the function even more, for instance checking if it really returns a promise and then waiting on it, otherwise do something else.

  3. Even if just by a bit, this pattern will add a bit of coupling to your solution, so you don't want to replace all events with callback functions!

  • This will work, but passing functions as props is an anti-pattern in Vue. – Coelacanth Mar 11 '21 at 15:29
  • I will kindly agree with you @Coelacanth that passing callbacks is a bad ideea in most cases. I will still make the case that this scenario is not one of those "most cases". Big libraries will implement this pattern to add guards and hooks to their components. See https://elementui.github.io/dev/master/#/en-US/component/dialog before-close prop. – Lupu Ștefan Alex Mar 11 '21 at 15:41
  • This pattern of passing methods as props rather than passing them as events that can be triggerred with emit worked better for me while resolving accessibility issues around setting focus after load or making a screen reader announcement using aria-live after async updates. The emit just didn't come to the rescue. – sidnc86 Nov 15 '22 at 06:02