Reacting to Promises from event listeners in Vue.js

Building a button component, that changes automatically on Promise resolution

Dobromir Hristov
Feb 7 · 4 min read

Users should know the outcome of an action they performed. A common UX pattern is buttons with loading, success and error states.

Changing those states on each action, be it easy with Vue, can be tedious, with allot of repeating code. You have to assign classes, change button text, revert the state and so on.

In this post I will show you how to leverage Promises and event handlers, to automatically change a button’s state. For styling I am using Bulma, but you can easily fit this to work with your custom styles.

This post was inspired from a recent fix in the release of Vue 2.6.

Usage of the component is straight forward

TLDR: Click handlers that return a promise, will be assigned using $listeners property and resolved inside the button component, changing its state based on the promise resolution. Button exposes slots for further customisation of states.

Important points

  1. Click handlers must return a promise in order for the button to react on it.
  2. Button calls the click handler from an internal method.
  3. Outer components must not know how the button handles promise states.
  4. Button should allow for customisation of each state

With those points laid out, lets build the button.

Returning the promise

First things first, returning the promise. Its a good practice to always return promises from functions, that way you can await them, if called from another function.

The above code is self explanatory, on each call of the onClick method, the promise from the async request is returned. If you want to catch potential errors and act accordingly, for the button to work, you need to re-throw the error again. Forgetting to do so will cause the button to show a successful state. This is not connected to building the button it self, rather how one could use it.

Using async/await syntax means you don’t need to return a promise, it will await accordingly. This can be seen in the asyncAction method.

Calling the handler from the button

To listen for clicks on the component, a normal click event handler is added to the button. The more complex part is passing all event listeners to the button, while overriding the click listener.

To do this, we use the handy $listeners property on the instance. It gives us reference to all the event listeners bound to the component, in an easy to use object syntax. Link to docs

<button v-on="listeners" ...

From the script above, we see that we use v-on to bind listeners, which is a computed property that passes all the parent event bindings, by spreading the object into a new one, overriding the click handler with a custom handleClick method on the component.

computed: {
listeners() {
return {
...this.$listeners,
click: this.handleClick
}
}
}

This way we can not only handle clicks, but also hover and other events. Those however, will not track promise resolution, as I think its not a common use case to do so.

Handling the state

The handleClick method is an async function, that warps everything in a try/catch block, setting an internal isLoading property and invoking the passed click event handler via await this.$listeners.click() . We are essentially accessing the click property from the $listeners object, which is the click handler passed from the parent. Remember that we are not passing it directly, as we overrode it inside the listeners computed property.

Making the method async is a handy, as that allows to await the click handler, even if it does not return a promise. To learn more about async/await check this article.

After the promise is resolved, we call a small helper method called resetDelayed which sets a property called isSuccess to true, reverting it back to false after X amount of time. This way we can return the button it its initial state while giving the user enough time to notice the change. In the catch block, we use the same method to set the hasError property.

In the finally block we just set the isLoading to false, indicating the end of the loading state.

The resetDelayed method sets a timeout of 2s, via the time prop, after which it returns the passed property to false. One thing to mind here, is that it checks if a loading prop is passed to the component via:

this.$options.propsData.hasOwnProperty('loading')

That allows for manual override of the loading state on the button from the parent component. In such case, we probably don’t care how the promise resolves, so we skip setting isLoading or hasError entirely

Customising the button states and behavior

To provide some customisation, we can use slots to give the user a way to pass custom icons or text for each of the possible states. We also provide the loading prop to set the loading state manually if needed. We can even override the amount of time the button stays in success of failure state via the time prop.

The computedClasses is a computed property, applying styling classes based on the current state the button is in. The loadingState is just a helper computed property, returning the the loading prop if passed, otherwise returns the internal isLoading property.

Dobromir Hristov

Written by

I am a JavaScript developer with a passion for tech.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade