Wait Time

Ten years on from the introduction of AJAX, the web is now full of asynchronous operations. AJAX itself made those async requests feasible, while Promises have been around for a while now to make handling async responses simple. When working with single-page architecture (SPA) web apps, everything is an async request.

What was lost in the move from synchronous requests to single-page architectures with async requests was the page refresh. Yes, that's the entire point of an SPA web app. But through experience, web users have come to understand intuitively that a browser will reload as a response to their actions–whether that action is clicking a link or submitting a form. Without that familiar refresh, a web app can feel unresponsive as the front end code waits for the result of an AJAX request.

This appearance of unresponsiveness becomes especially problematic when submitting forms. Users can easily become convinced that the app didn't register their previous interaction, and attempt to resubmit the form. The repercussions can range from relatively benign (duplicate, identical PUT requests that just waste server processing time) to significantly deleterious (paying for the same item multiple times).

At Made By Munsters, we specialize in developing single-page applications, and therefore we run across this issue often. To solve the problem for form submission, we created the Angular Waiting Button component. The directive can be attached to any button or link that kicks off an async operation and automatically handle presenting a "waiting" state. Waiting Button has these features:

  • Can start and listen to the status of any method that returns a Promise.
  • While in waiting state, prevents subsequent clicks from starting new operations.
  • Presents the button in a "waiting" state while the operation is in-flight.
  • Can show "success" or "failure" states depending on the result of the operation.
Installation

Installation is simple. Just add the angular-waiting-button npm module to your app:

npm i --save angular-waiting-button

Once installed, you can then import the button's module and add that as a dependency for your main app module:

import mbmWaitingButton from "angular-waiting-button";

angular.module("app", [mbmWaitingButton]);
Basic timeout example

I've set up a basic example in this webpackbin. It's a very simple Angular app with a single component named test. The test component controller has a function, fakeAsyncOperation, which registers a delayed function execution via Angular's stock $timeout service. fakeAsyncOperation returns the Promise created by $timeout so that the waiting button directive can respond to the status of the Promise.

Here is the barebones HTML skeleton:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
  </head>
  <body ng-app="app">
    <test></test>
    <script src="main.js"></script>
  </body>
</html>

And the full JavaScript setup:

import angular from "angular";
import mbmWaitingButton from "angular-waiting-button";
import './styles.scss';

angular.module("app", [mbmWaitingButton]);

class TestCtrl {
  constructor($timeout) {
    this.$timeout = $timeout;
  }

  fakeAsyncOperation() {
    console.log('fakeAsyncOperation::start');
    return this.$timeout(() => console.log('fakeAsyncOperation::complete'), 2500);
  }
}

angular
  .module("app")
  .component('test', {
    controller: ['$timeout', TestCtrl],
    template: `
      <button class="button" mbm-waiting-button="$ctrl.fakeAsyncOperation()">
        Wait For It...
      </button>
    `
  });

Note that the waiting button directive is configured to kick off, and listen to the results of, TestCtrl's fakeAsyncOperation method.

To help illustrate the waiting state, we have a very simple pulse animation set to being when the class mbm-waiting-button--waiting is added to the button:

$button-color: #cecece;

@keyframes pulse {
  0% {
    background-color: darken($button-color, 30%)
  }
  50% {
    background-color: darken($button-color, 15%)
  }
  100% {
    background-color: darken($button-color, 30%)
  }
}

.button {
  border-radius: 4px;
  border: none;
  background-color: #cecece;
  padding: 10px 20px;

  &.mbm-waiting-button--waiting {
    animation: pulse 1.5s infinite;
    cursor: wait;
  }
}

Clicking on the demo will kick off the $timeout service and waiting button will add the mbm-waiting-button--waiting class, starting the pulsing animation. If you have your developer console open, you'll see fakeAsyncOperation::start followed by fakeAsyncOperation:complete once the timeout has completed. If you try to click again while the button is in the waiting state, no further fakeAsyncOperation::start calls–waiting button prevents further calls to fakeAsyncOperation.

Showing operation state

Waiting button has a helper directive, waitingButtonText, that allows the directive to show different elements based on the current state of the waiting button's backing operation. It recognizes four states:

  • init - The backing operation has yet to be called (via the waiting button).
  • wait - The backing operation is in-flight.
  • success - The backing operation has successfully completed.
  • error - The backing operation has failed.

A button similar to the one we built for the first demo would look like this, if it had text values for all four states:

<button class="button" mbm-waiting-button="$ctrl.fakeAsyncOperation()">
  <span mbm-waiting-button-text waiting-button-state="init">Init</span>
  <span mbm-waiting-button-text waiting-button-state="wait">Waiting</span>
  <span mbm-waiting-button-text waiting-button-state="success">Success</span>
  <span mbm-waiting-button-text waiting-button-state="error">Error</span>
</button>

We can best see those states in effect with a simple demo using a timeout and a simulated coin flip. Depending on the result of the coin flip, the Promise created by the async operation (again created via $timeout) will be rejected or resolved. You can see it in action in this webpackbin demo.

You can also combine states by listing them separated by commas:

<button class="button" mbm-waiting-button="$ctrl.saveForm()">
  <span mbm-waiting-button-text waiting-button-state="init,error">Save</span>
  <span mbm-waiting-button-text waiting-button-state="wait">Saving</span>
  <span mbm-waiting-button-text waiting-button-state="success">Saved</span>
</button>
Roadmap

We've got a few more nifty features planned for waiting button. We want to better support forms, disabling the button when the form is invalid and better responding to form submit events. It'd also be great to have some default waiting animations to choose from, and the same goes for disabled states. We hope to be rolling out these new features in the near future. In the meantime, check out the demos and give it a try in your next projects. Let us know what you think in the comments below.

More posts
  • Getting Around API Rate Limiting

    Anyone who has had to work with an external API knows the problems that can occur when there are a lot of users hitting endpoints that directly pull from the external API's.

    Read More
  • Welcome Back TulsaJS

    I have always been a person looking to find my place within the development community. I am a fan of meetups and I am a big supporter of the meetup community in Tulsa, Okla. However, when it comes to a direct community around one specific language, Tulsa falls short.

    Read More
  • Quick Guide To Prototyping With Google Sheets

    Here's the problem. Designing prototypes for clients in Sketch, Photoshop or other tools don't always show realistic expectations of what the real-world product will represent. These tools are powerful, helpful and easy to use - for the most part.

    Read More