17

I'm dynamically updating a few elements after a setTimeout() function. The jQuery function .text() seems to dynamically update with each change of index of an array while processing. But a bootstrap progressbar which is being changed through .css() and .attr() doesnt seem to dynamically update. Here is my page : http://imdbnator.com/process.php?id=f144caf0843490c0d3674113b03da0c5&redirect=false

You can see that the text gets changed but the progress bar only finishes after the whole setTimeout() function finishes. Also, if I set the delay = 1000. It works. But it slows down by application. Therefore, I need delay = 0. But why doesnt the progressbar change?

Here is my snippet

function launch(a) {
    var inc = 0;
    var maxc = a.length;
    var delay = 0; // delay milliseconds
    var iID = setInterval(function () {    
        var index = inc;
        var movie = a[inc];    
        //start processing function    
        //styling while processing                
        var markerPer = ((index + 1) / rawListNum) * 100; // marker percentage
        $("#procNum").text("(" + (index + 1) + "/" + rawListNum + ")"); //Processing number
        $("#procMovie").text(movie); //Processing Name
        $("div[role='progressbar']").css("width", markerPer + "%").attr("aria-valuenow", markerPer); // progress bar -> DOES NOT WORK    
        if (++inc >= maxc) clearInterval(iID);    
    },
    delay);
}
  • 2
    The comments provided on SO here may be of help; [http://stackoverflow.com/questions/5743428/javascript-progress-bar-not-updating-on-the-fly-but-all-at-once-once-process](http://stackoverflow.com/questions/5743428/javascript-progress-bar-not-updating-on-the-fly-but-all-at-once-once-process). – Aaron May 17 '15 at 09:27
  • But that doesnt explain why the text changes but not a css element –  May 17 '15 at 09:56
  • Have you tried [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) instead of `setInterval`? – Kiril May 19 '15 at 09:33

6 Answers6

9

By way of explanation, a lot of answers are going to point out the fact that most browsers run JavaScript on the UI thread, so they cannot update the interface when JavaScript is being executed. Instead, the browser will wait for the Javascript to finish and return control to the UI thread.

While this is important to keep in mind, it doesn't come into play for your code. If you were updating the progress bar doing something like this:

for (i = 0; i < len; i++) {
    updateProgressBar(i)
};

Then it would definitely prevent the UI from updating until the end.

But you're already doing the right thing by using setTimeout or setInterval, which have asynchronous callbacks into the code. Meaning that the JavaScript is able to pause for long enough to pipe out any UI messages. As evidenced by the fact that the text is able to update dynamically.

As your question asks, why, if the UI is getting updated, is the progress bar not updated as well?

The answer lies in the fact that Bootstrap applies the following CSS to the progress bar:

.progress-bar {
  -webkit-transition: width .6s ease;
       -o-transition: width .6s ease;
          transition: width .6s ease;
}

When the delay between calls is 0ms, the browser does have time to update the UI, but does not have time to complete the 600ms CSS transition. Consequently, you don't see the animation until the end.

As OhAuth points out, you can prevent the browser from delaying the width update by removing the transition effect with CSS like this:

.progress .progress-bar {
  -webkit-transition: none;
       -o-transition: none;
          transition: none;
}

If you're looping through a lot of items, the incremental updates will provide some sort of animation. If you wanted to leave on the styling for a generic use case, you could toggle the transitions off and on in your code. As of jQuery 1.8 you can update CSS properties without the vendor prefix, so you'd just need to call .css("transition","none").

Of course, depending on how much work you're doing for each movie, the JavaScript may be able to finish before you can visually detect all of the UI changes, in which case extending the delay wouldn't hurt. If you'd like to test out longer running processes, you can use the following sleep function:

function sleep(sleepyTime) {
  var start = +new Date;
  while (+new Date - start < sleepyTime){}
}

Here's a demo you can play around where each setting is a variable that you can configure to see how it would react, but the best bet is just to conditionally remove transitions.

Demo in Stack Snippets

var delay, numMovies, throttledMovies, sleepTime, removeTransition;

$("#delay").change(function(){ delay = this.value }).change()
$("#movies").change(function(){ numMovies = this.value }).change()
$("#sleep").change(function(){ sleepTime = this.value }).change()
$("#transition").change(function(){ removeTransition = this.checked }).change()

$("#loadMovies").click(loadMovies);

function loadMovies() {
  var i = 0;
  
  throttledMovies = Movies.slice(0, numMovies)
  
  if (removeTransition) {
    $('#progress-bar-movie').css("transition","none");
  }
  
  var interval = setInterval(function () {
    loadMovie(i)

    if (++i >= numMovies) {
      clearInterval(interval);
      $('#progress-bar-movie').css("transition","width .6s ease");
    }
  }, delay);
};

function loadMovie(i) {
  var movie = throttledMovies[i];
  var percentComplete = ((i + 1) / numMovies) * 100;

  sleep(sleepTime);
  
  // update text
  $("#procNum").text("(" + (i + 1) + "/" + numMovies + ")");
  $("#procMovie").text(movie.Title); 

  // update progress bar
  $('#progress-bar-movie')
    .css('width', percentComplete+'%')
    .attr('aria-valuenow', percentComplete);
};

function sleep(sleepyTime) {
  var start = +new Date;
  while (+new Date - start < sleepyTime){}
}
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/css/bootstrap.css" rel="stylesheet"/>

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.2/js/bootstrap.js"></script>
<script src="http://kylemitofsky.com/libraries/libraries/movies.js"></script>

<!-- params -->
<label for="delay">Delay (ms)</label>
<input type="number" value="0" min=0 max=1000 id="delay"/>
<label for="movies"># Movies </label>
<input type="number" value="250" min=0 max=250 id="movies"/>
<label for="sleep">Sleep time (ms)</label>
<input type="number" value="0" min=0 max=1000 id="sleep"/>
<label for="transition">Remove transition? </label>
<input type="checkbox" checked="true" id="transition"/><br/><br/>

<button class="btn btn-primary btn-lg" id="loadMovies">
  Load Movies
</button><br/><br/>

<p>
  <b>Current Title</b>: <span id="procMovie"></span><br/>
  <b>Progress</b>: <span id="procNum"></span>
</p>

<div class="progress">
  <div class="progress-bar" role="progressbar" id="progress-bar-movie" 
       aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
Community
  • 1
  • 1
KyleMit
  • 30,350
  • 66
  • 462
  • 664
  • Thank you for your clear explanation. I would like to give the bounty ro OAuth though for point it initially. –  May 26 '15 at 14:18
  • @SaiKrishnaDeep. Great Idea! Happy for them to take the credit for cracking the nut. – KyleMit May 26 '15 at 14:34
3

It is due to the way bootstrap animates changes in progress bar state. If the timeout interval is smaller than the animation time it will queue the redraw.

Try adding this to your progress bar's CSS:

-webkit-transition: none;
transition: none;

Check my fiddle

thodic
  • 2,229
  • 1
  • 19
  • 35
  • @SaiKrishnaDeep My apologies I linked to the wrong fiddle, see my edit. – thodic May 19 '15 at 10:11
  • Perfect! But how do I make sure the animation is still there? As far as I understand your answer, the transition time should be lower than the delay if I'm not wrong. –  May 19 '15 at 10:13
  • Your fiddle works for me w/o adding `transition: none;` also. – Aleksandr M May 19 '15 at 10:20
  • It works on my website too when I made the change. I'll cross check again. In the mean time @OhAuth can you please answer my doubt in the above comment –  May 19 '15 at 10:23
  • @SaiKrishnaDeep My solution would be to remove animation using the CSS tags in my answer as matching your timeout to the animation time will likely cause issues. – thodic May 19 '15 at 10:26
  • @AleksandrM Setting both properties ensures cross browser compatibility. – thodic May 19 '15 at 10:27
  • I meant that I removed both `-webkit-transition` and `transition` and it still works. – Aleksandr M May 19 '15 at 10:28
0

Another work around (with reference to my previous answer):

If you must keep the animation, you could use a conservative interval delay of say 100 milliseconds. A delay which shouldn't be noticeable to the user.

Community
  • 1
  • 1
thodic
  • 2,229
  • 1
  • 19
  • 35
0

If you really need the delay to be zero for the calculations then I would separate the calculations and the setting of the progress bar value so that you are calculating the value in one setInterval, and then run a separate setInterval that updates the bar every 100 ms to this value.

This way your calculations are up to date and your UI has time it needs to update as well.

If you want them both in the same method then I think you need to add a minimum 100 ms delay.

Example: https://jsfiddle.net/dk7g51g4/3/

Javascript:

var _ValueToSet = 0;
$(document).ready(function () {

    // SET VALUE EXTREMELY FAST (your current code)
     setInterval(function () {
        _ValueToSet = Math.round(Math.random() * 100); // get value we are going to set
    }
    , 0); // run as fast as possible

    // RUN UI UPDATE AT 100MS (separate thread for ui updates).
    setInterval(function () {
        setRandom()
    }
    , 100); // give ui 100ms to do its thing
});

function setRandom() {
    var elem = $('.progress-bar'); // what is our target progress bar

    // set both style width and aria-valuenow
    elem.css('width', _ValueToSet + '%');
    elem.attr('aria-valuenow', _ValueToSet);
}

HTML:

<div class="progress progress-striped active">
    <div class="progress-bar" 
        style="width:0%" 
        role="progressbar" 
        aria-valuenow="0" 
        aria-valuemin="30" 
        aria-valuemax="100">
    </div>
</div>
JensB
  • 6,663
  • 2
  • 55
  • 94
  • 1
    When down voting, please state why. I would love to hear a good logical argument for why this is a bad idea :) – JensB May 19 '15 at 12:05
0

This is probably a redraw issue. Since the delay is set to 0 the CSS changes aren't drawn until the last update.

The easiest way to force a redraw is to read the elements height:

$("div[role='progressbar']").css("width", markerPer + "%").attr("aria-valuenow", markerPer);
$("div[role='progressbar']").height();
thodic
  • 2,229
  • 1
  • 19
  • 35
pstenstrm
  • 6,339
  • 5
  • 41
  • 62
0

Text and Progress Bar change dynamically, but with delay === 0 it's very fast: (http://jsfiddle.net/sergdenisov/mh4o1wxz/1/):

HTML:

<div class="text">Text</div>
<div class="progress">
    <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0;">
    </div>
</div>

Javascript:

var delay = 0;
var percent = 0;

var iID = setInterval(function() {
    percent += 10;
    $('.text').text('Text ' + percent + '/100');
    $('.progress-bar').css('width', percent + '%').attr('aria-valuenow', percent);    
    if (percent === 100) {
        clearInterval(iID);
    }
}, delay);
Alex
  • 8,461
  • 6
  • 37
  • 49
sergdenisov
  • 8,327
  • 9
  • 48
  • 63