17

I have a progress bar that I update in a loop of many iterations.

https://jsfiddle.net/k29qy0do/32/ (open the console before you click the start button)

var progressbar = {};

$(function () {

    progressbar = {

        /** initial progress */
        progress: 0,

        /** maximum width of progressbar */
        progress_max: 0,

        /** The inner element of the progressbar (filled box). */
        $progress_bar: $('#progressbar'),

        /** Set the progressbar */
        set: function (num) {
            if (this.progress_max && num) {
                this.progress = num / this.progress_max * 100;
                console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);

                this.$progress_bar.width(String(this.progress) + '%');
            }
        },

        fn_wrap: function (num) {
            setTimeout(function() {
                this.set(num);
            }, 0);
        }

    };

});


$('#start_button').on('click', function () {

    var iterations = 1000000000;

    progressbar.progress_max = iterations;

    var loop = function () {

        for (var i = 1; i <= iterations; i++) {

            if (iterations % i === 100) {

                progressbar.set(i); //only updates the progressbar in the last iteration

                //progressbar.fn_wrap(i); //even worse, since no output to the console is produced

            }
        }
    }

    //setTimeout(loop, 0);
    loop();

});

The console is updated iteratively as expected. However, the progressbar is not updating.

The problem is that the browser window seems to 'hang' until the loop finishes. Only the console is updated, not the progressbar.

I have tried to add the setTimeout, as suggested below, in several places. But that just makes things worse, because I then do not even get the console to output the progress while executing the loop.

Lordbalmon
  • 1,634
  • 1
  • 14
  • 31
reggie
  • 3,523
  • 14
  • 62
  • 97
  • What is the range of your index value? And do you intend to divide by 100 as part of setting the value? You may be trying to set your progressbar with a decimal value if that is the case, but you have explained it to work off of whole numbers between 0 and 100. – Jonathan Ochs Jun 22 '15 at 18:48
  • It was just a simplification. I can see in the console output that the progressbar is called with the right values. I have replaced the whole code in the loop with `my_progressbar_element.innerHTML = 'update';`. It only shows this text once the loop is finished. – reggie Jun 22 '15 at 20:05
  • Your div's width is updating as the console value. Just add the background-color:red in your progress bar div and see. – blue Jul 27 '15 at 12:17
  • @blue No, that's not the problem. DOM updates are blocked while the loop is running. I need to find a way around that. – reggie Jul 27 '15 at 13:21
  • How is the progress updated in the real application? – amiuhle Jul 29 '15 at 16:31

10 Answers10

12

Okay, I found a solution in the answer to this question:

Javascript: How to update a progress bar in a 'for' loop

var i = 0;
(function loop() {
    i++;
    if (iterations % i === 100) {
        progressbar.set(i); //updates the progressbar, even in loop    
    }   
    if (i < iterations) {
        setTimeout(loop, 0);
    }
})();

My solution: https://jsfiddle.net/ccvs4rer/3/

Community
  • 1
  • 1
reggie
  • 3,523
  • 14
  • 62
  • 97
  • I did? Well, the solution I went for was a different one entirely. It involved jQuery's queue(), setTimeout() and dequeue(). – reggie Aug 03 '15 at 21:11
8

Lets break this down to steps

Step 1: Clean up HTML

Assuming the purpose of your question is to understand how to work the progress bar and not the styles or the labels (loading, please be patient, etc.). Lets just have the progress bar and the start button.

<div id='progressbar-outer' style="">
    <div id='progressbar' style=""></div>
</div>
<button id="start_button">Start</button>

Step 2: The Styles

Lets make the progress bar visible to the user

#progressbar-outer {
    height:2em;
    border:5px solid #000;
    width:15em;
}
#progressbar {
    width:0%;
    background-color:#F00;
    height:100%;
}

Step 3: Using setTimeout where it belongs

In your code, you have used setTimeout to set the value of your progress bar. However, the for loop is still active.

for (var i = 1; i <= iterations; i++) {

    if (iterations % i === 100) {

        progressbar.set(i); //only updates the progressbar in the last iteration

        //progressbar.fn_wrap(i); //even worse, since no output to the console is produced

        //setTimeout(function() {
        //  progressbar.set(i);
        //}, 0);

    }
}

The use of setTimeout does not affect the rest of the code. Hence, the UI was held hostage till the loop ended. Try the following code.

$('#start_button').on('click', function () {

    var iterations = 100;

    progressbar.progress_max = iterations;

    var loop = function (value) {
        progressbar.set(value);
        if (value < iterations) setTimeout(function () {
            loop(value + 1)
        }, 30);
        else $('#progressbar').css('background-color', '#0F0');
    }


    loop(1);

});

Preview

Try this fiddle: https://jsfiddle.net/Ljc3b6rn/4/

Lordbalmon
  • 1,634
  • 1
  • 14
  • 31
7

What you really want is an Asynchronous loop to allow the browser to update the DOM in between iterations.

JSFiddle: http://jsfiddle.net/u5b6gr1w/

function delayedLoop(collection, delay, callback, context) {
    context = context || null;

    var i = 0,
        nextInteration = function() {
            if (i === collection.length) {
                return;
            }

            callback.call(context, collection[i], i);
            i++;
            setTimeout(nextInteration, delay);
        };

    nextInteration();
}

Some HTML:

<div class="progress-bar"><div style="width: 0"></div></div>

A splash of CSS:

.progress-bar {
    border: 1px solid black;
    background-color: #f0f0f0;
}
.progress-bar div {
    background-color: red;
    height: 1.25em;
}

And some JavaScript to wire things together:

var progressBar = document.querySelector(".progress-bar div"),
    items = [1,2,3,4,5,6,7,8,9,10];

delayedLoop(items, 500, function(item, index) {
    var width = (item / items.length * 100) + "%";
    progressBar.style.width = width;
    progressBar.innerHTML = width;
});
Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92
  • I tried a few different approaches to using setTimeout and setInterval but this is the only one that worked for me where i have a long running function with multiple large arrays to loop through and an ajax call. – Shiloh Jan 09 '16 at 19:44
6

My guess would be that all your progress updates are running in the same call stack. While JavaScript code is running, the DOM cannot update. Maybe this question will help you come up with a work-around.

Community
  • 1
  • 1
Adam Boduch
  • 11,023
  • 3
  • 30
  • 38
  • I've heard of setting setTimeout to zero before. It sounds like a plausible solution. But when I implemented something similar as suggested here http://stackoverflow.com/a/5226333/426266 , it did not work. I still see only output in the console. The DOM is updated once the loop is finished. – reggie Jun 22 '15 at 20:38
6

What do you wnat to do? Why do you need it? You should only use a progressbar when you have to wait for something to finish. But we don't know what you do on your page.

  1. If you want to display the progress of an ajax upload:

    $.ajax({
        ...
        xhr: function() {
            var xhr = $.ajaxSettings.xhr();
            $(xhr.upload).bind("progress", function(event) {
                var e = event.originalEvent;
                var percent = 0;
                if (e.lengthComputable)
                    percent = Math.ceil(e.loaded/e.total*100);
                $("#progress").width(percent+"%");
            });
            return xhr;
        }
        ...
    });
    
  2. For images, you need an ajax call:

    $.ajax({
        method: "GET",
        url: "http://example.com/path/image.jpg",
        xhr: function() {/* see the code above*/ }
        ...
    });
    
  3. For getting the content of an uploaded file:

    var reader = new FileReader();
    reader.readAsText(uploadedFile);
    $(reader).bind("progress", function(e) {
        var percent = 0;
        if (e.lengthComputable)
            percent = Math.ceil(e.loaded/e.total*100);
        $("#progress").css("width", percent+"%");
    });
    
  4. For large around of process, like math or appending a lot of divs that will take 10+ secons:

    Main.js:

    var worker = new Worker("Worker.js");
    $(worker).bind("message", function(data) {
        $("#progress").width((data*100)+"%");
    });
    

    Worker.js:

    var total = 43483,
        finished = 0,
        doStuff = function() {
            ++finished;
            return 1+1;
        };
    setInterval(function()
    {
        self.postMessage(finished/total);
    }, 100);
    for (var i = 0; i < total; ++i)
        setTimeout(doStuff, i*10);
    
  5. Because it's nice, and you want to tell the user there's a progress when there isn't, just animate the div:

    $("#progress").animate({width: "100%"}, 3000);
    
klenium
  • 2,468
  • 2
  • 24
  • 47
4

You can use promises to wait until the width is set before continuing the loop.
Updating the progress bar for 1000000000 iterations will be slow if you go 1 by 1, so you might find it useful to decrease the update frequency.
Instead of a for loop, I used a recursive function that loops when the promise has been fulfilled.

    set: function (num) { 
        var deferred = $.Deferred(); 
        if (this.progress_max && num) {
            this.progress = num / this.progress_max * 100;
            var self = this; 
            self.$progress_bar.animate({"width": String(this.progress) + '%'}, "fast", function() {  
                deferred.resolve(); 
            }); 
            return deferred; 
        }
    }

$('#start_button').on('click', function () {

    var iterations = 1000000000;
    var i = 0; 
    progressbar.progress_max = iterations;

    var loop = function(){
        i+=100000000; 
        if(i <= iterations){
            progressbar.set(i).then(function(){ 
                loop(); 
            }); ;         
        }
    }; 

    loop();
});

https://jsfiddle.net/k29qy0do/34/

html_programmer
  • 18,126
  • 18
  • 85
  • 158
3

You have to use window.requestAnimationFrame, otherwise the browser will block until your loop is finished. The callback passed to requestAnimationFrame will get a timestamp as a parameter which you might be able to use for calculations of the progress.

amiuhle
  • 2,673
  • 1
  • 19
  • 28
3

This are my 2 takes on the question:

Using a web worker. The webworker blob code comes from here

Web worker code:

<script type="text/ww">    
    function loop(e) {
        var data = JSON.parse(e.data);
        var i = parseInt(data.i, 10);
        var iterations = parseInt(data.iterations, 10);

        while (iterations % ++i !== 100 && i <= iterations);

        if(i <= iterations) {
            self.postMessage(JSON.stringify({ i: i, iterations: iterations }));
        }
    }

    self.onmessage = function(e) {
        loop(e);
    };
</script>

The code:

var ww = document.querySelector('script[type="text/ww"]'),
    code = ww.textContent,
    blob = new Blob([code], {type: 'text/javascript'}),
    blobUrl = URL.createObjectURL(blob),
    worker = new Worker(blobUrl);

worker.onmessage = function(e) {
    var data = JSON.parse(e.data);
    var i = parseInt(data.i, 10);
    var iterations = parseInt(data.iterations, 10);

    progressbar.set(i);

    worker.postMessage(JSON.stringify({ i: i, iterations: iterations }));
}

$('#start_button').on('click', function () {

    var iterations = 1000000000;

    progressbar.progress_max = iterations;

    worker.postMessage(JSON.stringify({ i: 0, iterations: iterations }));
});

The other idea hangs the UI thread, but changes the width visually, as I use requestAnimationFrame to break the counting, change width of the progressbar, and then continue the count.

function loopFrame(i, iterations) {
    requestAnimationFrame(function() {
        if (iterations % i === 100) {
            progressbar.set(i);
        }

        if(i < iterations) {
            loopFrame(i + 1, iterations);
        }
    });
}

$('#start_button').on('click', function () {
    var iterations = 1000000000;

    console.log(iterations);

    progressbar.progress_max = iterations;

    loopFrame(0, iterations);

});
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
1

Maybe this will be usefull.

var service = new Object();

//function with interrupt for show progress of operations
service.progressWhile = new Object();
service.progressWhile.dTime = 50; //step ms between callback display function
service.progressWhile.i = 0; //index
service.progressWhile.timer = 0; //start time for cycle

//@parametr arr - array for actions
//@parametr actionCallback - The function for processing array's elements
//@parametr progressCallback - function to display the array index

function progressWhile(arr, actionCallback, progressCallback) {
    try {
        var d = new Date();
        service.progressWhile.timer = d.getTime();
        log(service.progressWhile.i);
        if (service.progressWhile.i >= arr.length) {
            service.progressWhile.i = 0;
            return;
        }

        while (service.progressWhile.i < arr.length) {
            actionCallback(arr[service.progressWhile.i++]);
            d = new Date();
            if (d.getTime() - service.progressWhile.timer > service.progressWhile.dTime) {
                break;
            }
        }
        if (progressCallback != undefined)
            progressCallback(service.progressWhile.i);
    } catch (er) {
        log(er);
        return;
    }

    setTimeout(function () {
        progressWhile(arr, actionCallback, progressCallback);
    }, 0);
}
Rex
  • 36
  • 3
-2

Here's updated fiddle

I used animate to make it a progress bar like look and feel. Hope this will help you.

var progressbar = {};

$(function() {

    progressbar = {
        /** initial progress */
        progress : 0,
        /** maximum width of progressbar */
        progress_max : 0,
        /** The inner element of the progressbar (filled box). */
        $progress_bar : $('#progressbar'),
        /** Method to set the progressbar.*/
        set : function(num) {
            if (this.progress_max && num) {
                this.progress = num / this.progress_max * 100;
                console.log('percent: ' + this.progress + '% - ' + num + '/' + this.progress_max);

                $('#progressbar').animate({
                    width : String(this.progress) + '%',
                }, 500, function() {
                    // Animation complete.
                });
            }
        },

        fn_wrap : function(num) {
            setTimeout(function() {
                this.set(num);
            }, 0);
        }
    };

});

$('#start_button').on('click', function() {
    $('#progressbar').css('width', '0%');
    var iterations = 1000000000;
    progressbar.progress_max = iterations;
    var loop = function() {
        for (var i = 1; i <= iterations; i++) {
            if (iterations % i === 100) {
                progressbar.set(i);
                //only updates the progressbar in the last iteration
            }
        }
    }
    loop();
});

Fiddler

  [1]: https://jsfiddle.net/k29qy0do/21/
tnt-rox
  • 5,400
  • 2
  • 38
  • 52
blue
  • 932
  • 7
  • 9
  • So rather than link to an external fiddle (which you can do as well), list some code here and at least explain the relevant parts. If you really think you have a winning answer, then make it something worth others finding before that link dissapears. – Blakes Seven Jul 27 '15 at 13:18