Vue: get return value of $emit event's callback

Created on 14 Apr 2017  路  12Comments  路  Source: vuejs/vue

Version

2.2.6

Reproduction link

https://jsfiddle.net/50wL7mdz/27555/

Steps to reproduce

<component @click="callback">
</component>

function callbcak() {
  return new Promise(function(resolve, reject) {
    setTimeout(() => {
      resolve('resolved')
    }, 2000)
  })
}

// in component
<script>
  ...
    methods: {
      handleClick(evt) {
        var promise = this.$emit('click', evt)
        console.log(promise)   // promise is component self, not the return promise
      }
    }
  ...
</script>

What is expected?

expect $emit get the return value of event's callback

What is actually happening?

get component self

Most helpful comment

This would be a breaking change. Just emit a callback.

An alternative would be to use a common pattern from Web APIs where you provide a method on the emitted event called something like waitUntil that takes a promise.

For example:

<component @click="callback">
</component>

function callback(evt) {
  evt.waitUntil(new Promise(function(resolve, reject) {
    setTimeout(() => {
      resolve('resolved')
    }, 2000)
  }))
}

// in component
<script>
  ...
    methods: {
      handleClick(evt) {
        var promise = Promise.resolve()
        evt.waitUntil = p => promise = p
        this.$emit('click', evt)
        console.log(promise)   // promise is a Promise
      }
    }
  ...
</script>

If desired, you could get more robust and throw an error if waitUntil is called more than once.

All 12 comments

This would be a breaking change. Just emit a callback.

+1

This would be a breaking change. Just emit a callback.

An alternative would be to use a common pattern from Web APIs where you provide a method on the emitted event called something like waitUntil that takes a promise.

For example:

<component @click="callback">
</component>

function callback(evt) {
  evt.waitUntil(new Promise(function(resolve, reject) {
    setTimeout(() => {
      resolve('resolved')
    }, 2000)
  }))
}

// in component
<script>
  ...
    methods: {
      handleClick(evt) {
        var promise = Promise.resolve()
        evt.waitUntil = p => promise = p
        this.$emit('click', evt)
        console.log(promise)   // promise is a Promise
      }
    }
  ...
</script>

If desired, you could get more robust and throw an error if waitUntil is called more than once.

Nice vision @dlongley, you see beyond to the edge!!

You could also just pass the resolve parameter directly to $emit like so:

methods: {
    handleClick(evt) {
        var result = new Promise((resolve) => this.$emit('click', evt, resolve))

        result.then((value) => console.log(value))

        return result
    }
}

That way you're not mutating an object that you didn't create ;) The receiver would consume it as the second argument:

function callback(evt, resolve) {
    setTimeout(() => resolve('resolved'), 2000)
}

@sirlancelot,

This forces the receiver to call resolve in order to achieve proper operation and eliminates the ability for them to reject with sane (and automatic) error propagation.

The waitUntil approach allows the receiver to take some asynchronous action, but only if necessary, and provides for proper error propagation. I don't think there's a good reason to break the abstraction. Considerable thought was put into this design pattern in various Web APIs (e.g. ServiceWorkers) so I would recommend that approach. Of course, other patterns will work.

@dlongley awesome approach that I had not thought of yet!
Would it be possible to use that technique on something like this? I wrap callAfterLoggedIn around my function that I want to be retried if the token needed to be refreshed. The problem I'm having is that I need the value returned from the promise on the second try. The first try the value is returned properly. I can't find out how to return the promise after attaching it to the 'tokenRefreshed' event.

export function callAfterLoggedIn(fn){
    if(store.getters.loggedIn){
        return callTryTokenRefresh(fn)
    }else{
        store.state.bus.$once('loggedIn', fn)
    }
}

export function callAfterTokenRefreshed(fn){
    store.state.bus.$once('tokenRefreshed', fn)
}

export function callTryTokenRefresh(fn){
    let possiblePromise = fn()
    let promise = possiblePromise instanceof Promise
    if(promise){
        return possiblePromise.then(null, () => {
            return callAfterTokenRefreshed(fn)
        })
    }
    return possiblePromise
}

I recently had an issue where I needed to do this in one of my event buses. I wrote a wrapper around the standard $emit using some ideas from this thread. It tidies up the API, and also allows only a single invocation of the promise per emit. Maybe not a 'best practice', but it is 'totally practical' AFAIAC...

Here's a POC codepen: https://codepen.io/Flamenco/pen/deqPvy?editors=1111

/**
 * Adds an promise-like object with resolve/reject methods as the last argument, and returns a promise.
 * @param topic The topic to emit
 * @param varargs A 0..n arguments to send to the receiver
 * @return {Promise<any>} A promise with the result.
 * The receiver must call resolve or reject on the final argument.
 */
Vue.prototype.$emit_p = function (topic, varargs) {
  return new Promise((resolve, reject) => {
    const arr = Array.from(arguments)
    let invoked = false
    const promiseLike = {
      resolve: val => {
        if (!invoked) {
          invoked = true
          resolve(val)
        }
      },
      reject: val => {
        if (!invoked) {
          invoked = true
          reject(val)
        }
      }
    }
    arr.push(promiseLike)
    this.$emit.apply(this, arr)
  });
}

Publisher

this.$emit_p('add', 1, 2).then(res=>console.log(res))

Subscriber

this.$on('add', function(l,r,promise) {
    setTimeout(()=>promise.resolve(l+r), 1000)
})

Could you pass the callback function as a prop instead of an event listener, then call the prop, e.g.:

Parent:

<template>
  <v-child :callback="handleCallback" />
</template>

<script>
  export default {
    methods: {
      handleCallback() {
        return Promise.resolve();
      }
    }
  }
</script>

Child:

props: {
  callback: {
    type: Function,
    required: true,
  },
},

methods: {
  event() {
    this.callback().then(() => {});
  }
}

This makes sense to me given event emitters are agnostic to who's listening, but this _requires_ a return value

@samboylett that works for component based events but I needed something for an event bus that is stored in Vuex so this won't work

How to actually emit a callback? Examples from here doesn't work...

@yyx990803

This would be a breaking change. Just emit a callback.

I have such situation. Sometimes this.companies will get updates, sometimes not:
parent component

   fetchCompanies (resolve) {
      this.$store.state.backend
        .get('/jobBuilder/company/all')
        .then(ret => {
          console.log('companies fetched')
          this.companies = ret.data
          if(resolve){
            resolve('resolved')
          }
        })
        .catch(error => console.error(error))
    }

child component

    toggleActivation (button, company) {
      button.disabled = true

      let fetch = new Promise((resolve) => this.$emit('fetch', resolve)) //which activated fetchCompanies in parent

      this.$store.state.backend
        .post('/admin/update-activation/company', {
              id: company.id,
              active: !company.active
        })
        .then(() => fetch)
        .catch(err => alert(err))
        .finally(() => button.disabled = false) 
    }

And I'm not sure why, but API calls are not in the order I need them to be:

companies fetched
XHR finished loading: GET "http://localhost/jobBuilder/company/all"
companies watch activated
resolved
XHR finished loading: POST "http://localhost/admin/update-activation/company"

where it actually should be:

XHR finished loading: POST "http://localhost/admin/update-activation/company"
XHR finished loading: GET "http://localhost/jobBuilder/company/all"
companies watch activated
companies fetched
resolved
Was this page helpful?
0 / 5 - 0 ratings