Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get return value of $emit event's callback #5443

Closed
wang2lang opened this issue Apr 14, 2017 · 13 comments
Closed

get return value of $emit event's callback #5443

wang2lang opened this issue Apr 14, 2017 · 13 comments

Comments

@wang2lang
Copy link

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

@yyx990803
Copy link
Member

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

@alexsandro-xpt
Copy link

+1

@dlongley
Copy link

dlongley commented Apr 6, 2018

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.

@alexsandro-xpt
Copy link

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

@sirlancelot
Copy link

sirlancelot commented Apr 10, 2018

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

@dlongley
Copy link

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

@mycarrysun
Copy link

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

@leevigraham
Copy link

@Flamenco
Copy link

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

@samboylett
Copy link

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

@mycarrysun
Copy link

@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

@janswist
Copy link

janswist commented Mar 15, 2019

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

oilpal pushed a commit to oilpal/vuejs_ex that referenced this issue Aug 23, 2020
…ou 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)
oilpal pushed a commit to oilpal/vuejs_ex that referenced this issue Aug 23, 2020
vuejs/vue#5443 참조

You could also just pass the resolve parameter directly to $emit like so:
@hagabaka
Copy link

Is there a chance this could be reconsidered today, when async/await code is much more commonly used than callbacks? It may be a breaking change, but I find it hard to imagine why any code needs to rely on emit returning the component itself. Also, it's pretty confusing that you can (and need to) specify the return type of handlers in typescript defineEmits, which the IDE says will be the return type of emit, but the actual return value is different.

const emit = defineEmits<{
  (event: 'click') : Promise<void> // would be nice to be able to use v-on:click="fetchData" in consuming components
}>();
const loading = ref(false);
onNativeButtonClick() {
  loading.value = false;
  await emit('click'); // VSCode says const emit: (event: "click") => Promise<void>, but actually returns component as stated in first post
  loading.value = true;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests