2

What I'm trying to achieve is to queue up some animations (using CSS3 transition and addClass(), removeClass()) on an array of elements. I've tried multiple queueing mechanisms, but unfortunately they do not behave as expected.

The correct result is to add the 'btn-danger' class to a button, then when the transition end, remove the class. After that go to the next element and so on. This is a dynamic list so I can't hardcode the animations.

Here's some samples of my code:

function animateSelections(cb) {
    var randomIndexesArray = [];

    var studentsArray = $('#students-list .btn');
    var studentsArrayLength = studentsArray.length;

    function shuffleArray(array) {
        for (var i = array.length - 1; i > 0; i--) {
            var j = Math.floor(Math.random() * (i + 1));
            var temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
        return array;
    }

    shuffleArray(studentsArray);

    var chain = function(element, index) {
        if (element[index]) {
            $(element[index]).addClass('btn-danger').on('webkitTransitionEnd', function () {
                $(this).removeClass('btn-danger').on('webkitTransitionEnd', function () {
                    chain(element, index + 1);
                });
            });
        }
    };

    chain(studentsArray, 0);

    if (typeof cb != 'undefined') {
        cb();
    }
}
#students-list button {
    font-size: 14px;
    transition: all .3s;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="students-list">
    <div class="row">            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John 
    Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
</div><div class="row">            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
</div><div class="row">            <div class="col-xs-3">
                <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
            </div>
    </div>

    </div>

The current state of the code never finishes, I think there's a problem with the transitionend event

VladNeacsu
  • 1,268
  • 16
  • 33
  • If you know how long the transitions should last, just do a `setTimeout` and then remove/add class in the function – Canvas May 21 '17 at 20:31
  • The transition last 300ms, but I don't know how many elements the array has. Are you suggesting something like: `setTimeout(function() { $(element[index]).addClass('btn-danger'); setTimeout(function() { $(element[index]).removeClass('btn-danger'); }, 300); }, 300);` – VladNeacsu May 21 '17 at 20:32
  • 1
    I wouldn't go with setinterval or settimeout. The right way to do it is with the transitionend events https://jsfiddle.net/46jks8jk/1/ – João Paulo Macedo May 21 '17 at 21:13
  • I actually got it to work using setTimeout just after @Canvas posted the comment. @banzomaikaka I tried doing the same thing as you did in the fiddle, but didn't tought of using `one` instead of `on`. Can you explain why this happens? I saw that the transition end fires multiple times – VladNeacsu May 22 '17 at 11:09

2 Answers2

3

As long as you're using Bootstrap "default" styles, you're going to have problems animating changes of .btn from btn-something to btn-somethingElse. Because Twitter Bootstrap uses background-image property on top of background-color to create those gradient shadows. And background-image is not exactly animatable.

So you have two options:

  1. You have to remove the background-image property (see example below),

Example:

let interval = 300,
    duration = 300;

$('#students-list .btn').each(function(i, e) {
  $(e).css({
    transitionDuration:interval+'ms'
  }).addClass('removeBgImg');
  
  setTimeout(function() {
    $(e).removeClass('btn-primary').addClass('btn-danger');
    setTimeout(function(){
      $(e).removeClass('btn-danger').addClass('btn-primary');
    }, duration + interval)
  }, interval * i);
})
.btn {
  transition-property: background-color;
  transition-timing-function: linear
}
.btn.removeBgImg {
  background-image:none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<div id="students-list" class="container">
  <div class="row">
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John 
    Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
  </div>
</div>
  1. ...or, if you can't/don't want to, just create a copy of your button positioned absolute on top of the old one, fade it in, animating opacity. If you're using JavaScript to do it (which makes sense), take special care at cloning elements with IDs, because they issue some extremely hard to debug errors: proper order is:
    • store element ID,
    • remove ID from element,
    • clone,
    • add ID to clone,
    • fade clone in,
    • remove original.

Example:

let interval = 500,
    duration = 100;

$('#students-list .btn').each(function(i, e) {
  setTimeout(function() {
    let clone = fadeCloneIn($(e), 'btn-primary', 'btn-danger');
    setTimeout(function(){
      fadeCloneIn(clone, 'btn-danger', 'btn-primary')
    }, duration + interval)
  }, interval * i);
})

function fadeCloneIn(el, from, to) {
  let _t = this
  _t.cloner = $('<div />', {style:'position:relative;'});
  _t.id = el.attr('id') ? el.attr('id') : false;
  _t.style = el.attr('style') ? el.attr('style') : false;
  if (_t.id) el.removeAttr('id');
  _t.clone = el.clone(true).removeClass(from).addClass(to).css({
    opacity:0,
    position:'absolute'
  }).appendTo(_t.cloner);
  if (_t.id) _t.clone.attr('id', _t.id);
  _t.cloner.insertBefore(el);
  _t.clone.animate({opacity:1}, interval, function(){
    el.remove();
    $(this).removeAttr('style').unwrap();
    if (_t.style) $(this).attr('style', _t.style);    
  });
  return _t.clone;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<div id="students-list" class="container">
  <div class="row">
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John 
    Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
    </div>
    <div class="col-xs-3">
      <button class="btn btn-block btn-lg btn-primary viable"> John Smith </button>
  </div>
</div>

Cloning is a bit more complex because you need to preserve existing id, style, data and events on the original element and pass them to the clone, making sure the cloning function doesn't modify anything. Also, with cloning, you need to return the clone from the cloning function, because it deletes the original element and therefore any subsequent animations would end up without a subject.


The simple way to stagger is using $.each(), because it creates the required closure internally. If you want/have to do it in vanilla, you'll have to use a classic for() and wrap setTimeout() in a closure to which you pass the current element, otherwise this becomes err!? and the staggered animation is gone.

Regarding the design pattern:

In general, when animating collections, if the interval/step is constant, binding the start of the next animation to the end of the previous is regarded as bad practice and a major limitation, because the step has to be longer than the duration. You only do this when the duration of the effect can vary significantly and usually you do it using a recurrent defer/promise construct.

But normally you should keep step (interval) and duration of effect in two separate variables. See my answer to a similar question for details.

Community
  • 1
  • 1
tao
  • 82,996
  • 16
  • 114
  • 150
  • Great answer, I know about the `background-image` issue, I actually didn't run into that because I use a custom theme, but nonetheless this is very insightful and the code actually works. That's what I ended up doing – VladNeacsu May 22 '17 at 11:07
  • I just added the second example, using cloning. That's always going to be more complex, as it needs special care not to lose anything along the way. As a rule of thumb, as much as possible, avoid cloning in `JavaScript`. In the end, you'll get more and better sleep at night :). You're quite welcome. – tao May 22 '17 at 11:15
1

jsFiddle: https://jsfiddle.net/coy55j06/1/

This should help you get on track

javascript

$(function() {
  var classes = ["btn-success", "btn-normal", "btn-error"];
  var arrayIndex = 0;

  var renderInterval = setInterval(function() {
    var $btn = $('.btn');
    $btn.removeClass(classes.join(" ")).addClass(classes[arrayIndex]);
    arrayIndex += 1;
    if (arrayIndex >= classes.length) {
      clearInterval(renderInterval);
    }
  }, 1000);
});

So as you can see I have declared an array at the start, this of course can be any size. Then I set a variable to store the setInterval, I do this so we can remove it once it has iterated over all classes in the array. I then tell the setInterval to update every seconds (you may want to change this to 300). then find the element. We then first remove all classes that may have been added from the array using the array.join() function and then add the needed class.

Canvas
  • 5,779
  • 9
  • 55
  • 98