vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.88k stars 546 forks source link

Feature Request: `$emit` to return a Promise notifying when the event handler has run #586

Closed AyloSrd closed 1 year ago

AyloSrd commented 2 years ago

What problem does this feature solve?

When working with Component Events emitted with $emit, we may need, once the callback function is executed, to chain another callback, or another Component Event. This is especially relevant when the event callback is an asynchronous one (e.g. an API call).

As of now, we can achieve this only by notifying the child component via additional props, that the first callback has been executed, and then react to the notification with another callback; this requires though a lot of boilerplate code, and can result in a very complex props flow and inner logic.

It would be great if $emit retuned a Promise, that resolved to true when the callback has been executed, or false in all the other cases: the callback is null or undefined, the event has a .once modifier and was executed previously, an error is thrown, etc.

It would allow to create components that are less dependent on their parents' input, to reduce the amount of boilerplate code, as well as the complexity coming from having to manage a more complex props flow, and to obtain a leaner inner logic. The "one source of truth" principle would remain unbreached, as the Promise is not returning the actual value of the callback, but only notifying its execution.

What does the proposed API look like?

In the child component, the easiest case scenario :

// ChildComponent
<template>
    <button v-on:click="handleClick">Post and Close</button>
</template>

<script>
export default {
  methods: {
    handleClick(a) {
        this.$emit('post')
          .then(res => res && this.$emit('close'))
    },
  }
}
</script>

or a slightly more complex one :

// ChildComponent
<template>
    <button v-on:click="handleClick">Post and Close</button>
</template>

<script>
export default {
  methods: {
    async handleClick(a) {
        const hasPosted = await this.$emit('post')
        await this.showSuccessMsg(hasPosted)
        if (hasPosted) this.$emit('close')
    },
    showSucessMsg(res) {
      /*
        show a success/fail msg
        and return a Promise that
        resolves in a couple of seconds
      */
    }
  }
}
</script>

and, in both cases the parent will have this:

// In ParentComponent
<ChildComponent @post="handlePost" @close="handleClose" />
LinusBorg commented 2 years ago

Note: If you define the event as a prop instead of an emit, you can do that today (onXXX props are what v-on:XXX is translated to by th compiler):

<template>
  <button v-on:click="handleClick">Post and Close</button>
</template>

<script>
export default {
  props: ['onPost', 'onClose'],
  methods: {
    handleClick(a) {
        this.onPost('post')
          .then(res => res && this.onClose())
    },
  }
}
</script>
or a slightly more

Usage in the parent would stay the same, as v-on:post will end up as an onPost prop on the generated vnode.

AyloSrd commented 2 years ago

@LinusBorg , thanks for your note. If I understand well what you suggest, I believe you can do it only if the two callbacks are already returning a promise, am I wrong ?

What I would like to add as feature is for $emit to return the promise regardless of whether the callback is async or not. Also the Promise should resolve in true or false according to whether the callback has run or not, without sharing the actual return of the callback (so that that info is not sneaked to the child in breach of the one source of truth principle).

LinusBorg commented 2 years ago

I understand. For now, you could write such an emit function yourself. Something like:

app.config.globalProperties.$myEmit = function (prop, ...args) {
  const prop = this.$props[prop]
  if (!prop || typeof prop !== 'function') { return Promise.resolve(false) }
  return Promise.resolve(prop(...args))
}

Usage:

<template>
  <button v-on:click="handleClick">Post and Close</button>
</template>

<script>
export default {
  props: ['onPost', 'onClose'],
  methods: {
    handleClick(a) {
        this.$myEmit('onPost')
          .then(res => res && this.$myEmit('onClose'))
    },
  }
}
</script>
oemer-aran commented 1 year ago

Does this feature have any priority in vue 3?

Since Vue's philosophy is "Props to pass down data, emits to pass up data", passing callback functions as props feel like an anti-pattern to me. It would be really great to be able to know if an emit was added to the component (such as $listeners in vue 2) and then being able to await the emit in the child component.

faizalayub commented 1 year ago

I have found a workaround; the pattern looks like this. @AyloSrd

//# Parent Level
<template>
        <customer-picker
           @post="doRequest">
       </customer-picker>
</template>

<script>
export default {
  methods: {
    doRequest: function(resolve){
        setTimeout(() => {
             resolve('response from request')
        }, 6000);
    }
  }
}
</script>
//# Child Level
<template>
    <button v-on:click="handleClick">Post and Close</button>
</template>

<script>
export default {
  name: 'customer-picker',
  emits: ['post'],
  methods: {
    async handleClick(a) {

        const dataset = await new Promise(resolve => {
              this.$emit('post', resolve);
        });

        //# Final
        console.log(dataset);
    },
  }
}
</script>