Angular Promise Queue


Angular Promise Queue

As the name implies, this module implements a promise queue using the Q library. You're probably thinking: "okay, promises are already in a queue-like format in the form of sequential thenables" but there are few instantces where greater control of the sequence would be beneficial to have.

For front-end development, a flexible CSS transition queue could be useful. But the question arises...how do you know when the previous CSS transition has ended and when to start the next one?

Well, that problem has already been solved for us and layering promises on top of that, we can avoid some bad ole' callback hell.

In order to highlight why a promise queue would be useful using actual code, I'll use the example shown in the codepen above. Simply, it is a sequenence of transitions that translate each line of text from left to right, or right to left, while increasing it's opacity. And, they will be invoked in order and one after another.

Before going too deep into it, I'd like to say that this implementation is modeled both after Bluebird Queue and the built-in jQuery queue. Promise Queue is also dependent on the $q module which is an angular version of the Q promise library. As an alternative to Promise Queue, you could also use the queue.js version supported by Q.


Demo

The first thing to show is the markup. word-slide is an angular directive that collects all it's children DOM element and appends a function to the Promise Queue that adds an 'enter' class to each and thus triggers a transition to translate. We'll be utilizing a Promise Queue here, the affect we'll be going after is one in which it appears as if all the words are
sliding in one after another.

<word-slide>  
  <div class="slideLeft">This</div>
  <div class="slideRight">is</div>
  <div class="slideLeft">an</div>
  <div class="slideRight">example</div>
  <div class="slideLeft">of</div>
  <div class="slideRight">a</div>
  <div class="slideLeft">promise</div>
  <div class="slideRight">queue</div>
</word-slide>  

Next, we should include promise-queue as a dependency to our angular module.

angular.module('starter', ['promise-queue'])  

Next, we'll be copying over a useful a polyfill snippet from David Walsh that assists in determining when a CSS transition has ended so that we can determine when to fire the next transition in the queue.

It loops over each known event name for a transition end, depending on the browser, and applies them to a fake element to see which name is supported. The result of which is attached to the transitionEvent variable. This is all dandy and good but there is one small hiccup to this function in that some browsers actually support more than one transition end event name and this causes a problem that will be addressed later on.

var transitionEvent = whichTransitionEvent();

function whichTransitionEvent(){  
  var t;
  var el = document.createElement('fakeelement');
  var transitions = {
    'transition':'transitionend',
    'OTransition':'oTransitionEnd',
    'MozTransition':'transitionend',
    'WebkitTransition':'webkitTransitionEnd'
  }

  for(t in transitions){
    if( el.style[t] !== undefined ){
      return transitions[t];
    }
  }
}

Now that we've determined which transitionEvent our browser supports, we will create a new instance of Promise Queue:

 var promiseQueue = new PromiseQueue();

We then create a higher-order function that accepts a DOM element as its only parameter and returns a function that adds an 'enter' class and a listener when the transitionEvent is called. Promise Queue automatically creates a new deferred when a function is added to the queue and it injects the resolve method of that deferred as the first parameter of the added function.

Yikes, that's a mouthful. More simply, make sure that you reserve the first parameter of your added function (as done if you'd like) and invoke it only when your function's internal process are to finish. For the purpose of this demo, we consider the internal process finished when the transition has completed.

var slideIn = function(el){  
  return function(done) {
    angular.element(el).addClass('enter').one(transitionEvent, function(e) {
      done();
    });
  }
}

I mentioned earlier that there is a small hiccup with the transitionEvent function we made earlier. That's resolved by using the JQlite method, .one(). It is used to listen on the the transitionEvent because some browsers support more than one transition event which will result in the callback being executed multiple times. one() ensures that it's only called once!

Next up, we need to define exactly what adding the class enter should do. In this case, when enter is added, it will trigger a transition to translate the element left (or right) while bringing it to full opacity.

.slideLeft {
  transform: translateX(-30px);
  opacity: 0;
}

.slideRight {
  transform: translateX(30px);
  opacity: 0;
}

.slideLeft.enter, .slideRight.enter {
  transform: translateX(0px);
  opacity: 1;
  transition: all 0.3s ease-out;
}

An instance of Promise Queue: Check. CSS transition animations classed: check. A function to define when a transition has ended: check.

Okay, now it's time to build the queue.

var elements = $element.children();

promiseQueue  
  .add(slideIn(elements[0]))
  .add(slideIn(elements[1]))
  .add(slideIn(elements[2]))
  .add(slideIn(elements[3]))
  .add(slideIn(elements[4]))
  .add(slideIn(elements[5]))
  .add(slideIn(elements[6]))
  .add(slideIn(elements[7]));

Note that $elements is the element returned in the link function of the word-slide directive. We just query it's children and enter them as the first parameter in the slideIn function.

And so the final order of business is to just add a click listener to a button that starts the Promise Queue.

var startButton = document.getElementById('start');  
angular.element(startButton).on('click', function() {  
  promiseQueue.start();
});

That concludes this particular demo. Below are full descriptions of all the methods attached to the Promise Queue object.

Demo GitHub

Methods

.add() - Adds a function to the queue. It optionally accepts an array of functions

  • @param {function} func function OR array of functions
  • @returns {this}
var promiseQueue = new PromiseQueue();

promiseQueue.add(function(done){  
  // something()
  done();
})

.start() - Adds a function to the queue. It optionally accepts an array of functions

  • @param {function} func function OR array of functions
  • @returns {this}
var promiseQueue = new PromiseQueue();

promiseQueue  
  .add(function(done){
    // something()
    done();
  })
  .add(function(done){
    // something()
    done();
  })
  .start();

.pause() - Pauses the queue

  • @returns {this}
var promiseQueue = new PromiseQueue();

promiseQueue  
  .add(function(done){
    // something()
    done();
  })
  .start();

  // later

promiseQueue.pause();  

.instant() - Puts the included function in the front of the queue

  • @param {function} func The function to be placed at the front of the queue
  • @returns {this}
var promiseQueue = new PromiseQueue();

promiseQueue  
  .add(function(done){
    console.log(1);
    done();
  })
  .add(function(done) {
    console.log(2);
    done();
  })

promiseQueue  
  .instant(function(done) {
    console.log(3);
    done();
  })
  .start();

  // logs 3 ==> 1 ==> 2

.drain() - The queue will be emptied. Any function currently in progress will be allowed to finish.

  • @returns {this}
var promiseQueue = new PromiseQueue();

promiseQueue.add(function(done){  
  // something()
  done();
})

promiseQueue.drain();

var inQueue = promiseQueue._queue.length // 0  

.remove() - Compares the provided function with all functions in the queue and removes them

  • @param {function} func The function to be removed from the queue
  • @returns {this}
var promiseQueue = new PromiseQueue();

var a = function(done){  
  // something()
  done();
});

var b = function(done){  
  // something()
  done();
});

promiseQueue.add([a,b]);

promiseQueue.remove(a);