Transition events

Nothing is more eye compelling then a nice animation don’t you think?
Up until few years ago most of web page animation where done by Flash or JavaScript libraries like jQuery.
Today we all know that using CSS3 we can add transition and event keyframe animations.

When I first started to work with transition I didn’t expect to find any problem (ye ye as long as I’m not using IE 8/9), However event transition are in satisfactory and cause problems.

W3C transition expose only one event transitionend and each browser support this event with a slight name change: transitionend , oTransitionEnd , webkitTransitionEnd

Simple transaction

If you apply a principle like “separation of concerns” when implementing both JavaScript and CSS3 you will end up writing something like this

function disableTillDoneAnimation(){
   $('#exit').attr("disabled", "disabled");
   $('#enter').attr("disabled", "disabled");
   $('.box').one('transitionend ',function(){
     //do some real work
     $('#exit').removeAttr("disabled");    
     $('#enter').removeAttr("disabled");    
   });
 }
$(function(){
  $('#exit').click(function(){
    $('.box').addClass('exitAnimation');    
    disableTillDoneAnimation();
  });
  $('#enter').click(function(){
    $('.box').removeClass('exitAnimation');
    disableTillDoneAnimation();
  });
});

Where the css is

.animationContainer{
  width:400px;
  height:100px;
  overflow:hidden;
  position:absolute;
}
.box{
  width:50px;
  height:50px;
  position:relative;
  /* vendor prefixes excluded for brevity */
  transition: left 2s ;
  left:0%;
}
.exitAnimation{  
    left:100%;
}

 

I used the “one” and not the “on” method so that the listener will be removed after the event is fired preventing getting called more than once.

The problem

So what is wrong with one event? Way do you need more? 
The problem is that it does not always get fired.

How is it possible that I add a transaction class and still the event doesn't get fired. 
The answer is written in the event documentation:

In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire
Meaning unless you know by advance that the change you are making to the DOM element will cause a transition you can’t rely on this event.

This mean that if the toggled class does not cause an animation due to user clicking the button twice in a row or the “other” programmer who created the css didn’t think about the animation you end up waiting for an event that will never happen.

IE solution

For me this is unacceptable. So I started looking for a way to always receive transitionend.
To my amazement IE had a solution. IE > 10 gives you transitionstart event that is called when the transition start.

With this ability we can change the code to

 function disableTillDoneAnimation() {
  $('.box').one('transitionstart ', function() {
    $('#exit').attr("disabled", "disabled");
    $('#enter').attr("disabled", "disabled");
  });
  $('.box').one('transitionend ', function() {
    //do some real work
    $('#exit').removeAttr("disabled");
    $('#enter').removeAttr("disabled");
  });
}

Chrome & Firefox solution

How can we solve this in other browsers? 
No build in solution is given so I have come up with 2 workaround and would love to hear more suggestion from anyone else.

Transition is performed on absolute values (number, percentage, color, length, etc…) and not on intermediate transition values (auto, block, etc …)

So why not calculate by ourselves the difference between the current element style and the style after transition.

Option 1


1.    We calculate the current element style using getComputedStyle.
2.    We add the class responsible for the animation
3.    getComputedStyle return the current used values of the elements so we must wait for a few mill-second more than the transition-delay to let the animation take effect. 
4.    We recalculate the element style.
5.    If there is a difference between the start and current style the animation has started and we can trigger our own custom transitionstart event.

$(function(){
  $('#exit').click(function(){
    
    var $box = $('.box');
    //step 1
    var baseStyle = getElementStyles($box[0]);
    
    disableTillDoneAnimation();
    
    //step 2
    $box.addClass('exitAnimation');
    var delay =  $box.css('transition-delay');
    delay = parseFloat(delay);
    //step 3
    setTimeout(function(){
      //step 4
      var animatedStyle = getElementStyles($box[0]);
      //step 5
      if (!$.isEmptyObject(styleDifference(baseStyle,animatedStyle))){
      $box.trigger("transitionstart");
    }
    },delay * 1000 + 100);
  });
});

//Method taken from jQuery ui
function getElementStyles( elem ) {
    var key, len,
        style = elem.ownerDocument.defaultView ?
            elem.ownerDocument.defaultView.getComputedStyle( elem, null ) :
            elem.currentStyle,
        styles = {};

    if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) {
        len = style.length;
        while ( len-- ) {
            key = style[ len ];
            if ( typeof style[ key ] === "string" ) {
                styles[ $.camelCase( key ) ] = style[ key ];
            }
        }
    } 

    return styles;
}

//Method taken from jQuery ui
function styleDifference( oldStyle, newStyle ) {
    var diff = {}, name, value;
    var shorthandStyles = {border: 1, borderBottom: 1, borderColor: 1, borderLeft: 1, 
      borderRight: 1, borderTop: 1, borderWidth: 1, margin: 1, padding: 1    }

    for ( name in newStyle ) {
        value = newStyle[ name ];
        if ( oldStyle[ name ] !== value ) {
            if ( !shorthandStyles[ name ] ) {
                if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) {
                    diff[ name ] = value;
                }
            }
        }
    }

    return diff;
}

Option 2

1.    We add the class responsible for animation.
2.    We change the element display to none
3.    We request the current computed style causing the browser to recalculate the element location while display is none thus skipping the animation.
4.    We return the element back to its original state by removing the class responsible for animation and changing the display property back.
5.    We again request the current computed style causing the browser to relies that everything is back to normal
6.    If the 2 calculated styles are different we can fire the transitionstart event.
7.    We add the animation class this time we let the browser to the real work.

$(function(){
  $('#exit').click(function(){
    
    var $box = $('.box');
    var currDisplayStyle = $box.css('display');
    
    //step 1
    $('.box').addClass('exitAnimation');
    //step 2
    $box.css('display','none');
    //step 3
    var animatedStyle = getElementStyles($box[0]);
    
    //step 4
    $box.removeClass('exitAnimation');
    $box.css('display',currDisplayStyle);
    //step 5
    var baseStyle = getElementStyles($box[0]);
        
    disableTillDoneAnimation();
    //step 6
    if (!$.isEmptyObject(styleDifference(baseStyle,animatedStyle))){
      $box.trigger("transitionstart");
    }
    //step 7
    $box.addClass('exitAnimation');

  });
});

Summary

What is still missing? transitioncancel.
The code is great but if something will cancel the transition after it started and before it end we will not get the transitionend event.
For this I have no solution other than monitoring the animation to make sure we still see changes between each interval.

 

Developer