Prevent transitionend event firing twice
I came across this problem when handling a CSS transitionend
event. The transitionend
event fires when a CSS transition has completed. The problem wasn't that it wasn't firing, it was firing, but twice.
Here is an example of my event handler.
$('.bacon').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function() {
console.log('Transition complete');
});
As you can see I am using jQuery .one()
. Unlike .on()
this handler is only executed once, so the fact that it was firing twice was really confusing. I have used this before in previous projects and not had this problem.
The answer
The clue was in the description from jQuery docs:
The handler is executed at most once per element per event type.
Once per element per event type. Logging the event type to the console makes it clear what is happening.
$('.bacon').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e){
console.log(e.type);
});
This is what was output in the console of Chrome dev tools.
transitionend
webkitTransitionEnd
Since Chrome 36, both webkitTransitionEnd
and transitionend
have been supported. Meaning that correctly, the event handler was firing once per event type.
The fix
To resolve this we need to test which event name the browser supports.
Modernizr has a method called Prefixed
which detects the CSS feature you pass into it and returns either the prefixed or nonprefixed property name. The docs use the following as an example.
var transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd',// Saf 6, Android Browser
'MozTransition' : 'transitionend', // only for FF < 15
'transition' : 'transitionend' // IE10, Opera, Chrome, FF 15+, Saf 7+
},
transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];
$('.bacon').one(transEndEventName, function() {
console.log('Transition complete');
});
Or without Modernizr (taken from David Walsh Blog):
function whichTransitionEvent() {
var el = document.createElement('fake'),
transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd',// Saf 6, Android Browser
'MozTransition' : 'transitionend', // only for FF < 15
'transition' : 'transitionend' // IE10, Opera, Chrome, FF 15+, Saf 7+
};
for(var t in transEndEventNames){
if( el.style[t] !== undefined ){
return transEndEventNames[t];
}
}
}
var transEndEventName = whichTransitionEvent();
$('.bacon').one(transEndEventName, function() {
console.log('Transition complete');
});
Either methods ensure that the transitionend
event is only fired once.
Note: From Firefox 49 onwards a number of webkit prefixed properties are supported (see Firefox 49 for developers compatibility section). This means Firefox supports webkitTransitionEnd
as well as transitionend
and either will work. Thanks to Jarno de Haan for pointing this out.
Summary
We often deal with vendor prefixes in CSS, aware that one day they will be replaced once implemented in the formal spec. But with no breaking changes to our CSS. However that is not the case with transition end events, and our once working code can behave in unexpected ways. The take away from this is it is wise to always detect for an event, property or feature instead of a capture all approach. This will ensure our code is future proof and stable.