I am trying to create a leaderboard when someone completes my quiz. I can get the person to enter their name via a prompt with their score when they complete quiz. I just can get the data to stay. I think I need to store the data with storage? Or is it there a better way to do this?
I added my code to codepen since it's kind of long:
https://codepen.io/rob-connolly/pen/xyJgwx
Edit: fixed broken codepen link
// variables
var score = 0; //set score to 0
var total = 10; //total nmumber of questions
var point = 1; //points per correct answer
var highest = total * point;
//init
console.log('script js loaded')
function init() {
//set correct answers
sessionStorage.setItem('a1', "b");
sessionStorage.setItem('a2', "a");
sessionStorage.setItem('a3', "c");
sessionStorage.setItem('a4', "d");
sessionStorage.setItem('a5', "b");
sessionStorage.setItem('a6', "d");
sessionStorage.setItem('a7', "b");
sessionStorage.setItem('a8', "b");
sessionStorage.setItem('a9', "d");
sessionStorage.setItem('a10', "d");
}
//hide all questions to start
$(document).ready(function() {
$('.questionForm').hide();
//show question 1
$('#question1').show();
$('.questionForm #submit').click(function() {
//get data attribute
current = $(this).parents('form:first').data('question');
next = $(this).parents('form:first').data('question') + 1;
//hide all questions
$('.questionForm').hide();
//show next question in a cool way
$('#question' + next + '').fadeIn(400);
process('' + current + '');
return false;
});
});
//process answer function
function process(n) {
// get input value
var submitted = $('input[name=question' + n + ']:checked').val();
if (submitted == sessionStorage.getItem('a' + n + '')) {
score++;
}
if (n == total) {
$('#results').html('<h3>Your score is: ' + score + ' out of ' + highest + '!</h3> <button onclick="myScore()">Add Your Name To Scoreboard!</a>')
stop()
}
return false;
}
window.yourPoints = function() {
return n;
}
function myScore() {
var person = prompt("Please enter your name", "My First Name");
if (person != null) {
document.getElementById("myScore").innerHTML =
person + " " + score
}
}
var x;
var startstop = 0;
window.onload = function startStop() { /* Toggle StartStop */
startstop = startstop + 1;
if (startstop === 1) {
start();
document.getElementById("start").innerHTML = "Stop";
} else if (startstop === 2) {
document.getElementById("start").innerHTML = "Start";
startstop = 0;
stop();
}
}
function start() {
x = setInterval(timer, 10);
} /* Start */
function stop() {
clearInterval(x);
} /* Stop */
var milisec = 0;
var sec = 0; /* holds incrementing value */
var min = 0;
var hour = 0;
/* Contains and outputs returned value of function checkTime */
var miliSecOut = 0;
var secOut = 0;
var minOut = 0;
var hourOut = 0;
/* Output variable End */
function timer() {
/* Main Timer */
miliSecOut = checkTime(milisec);
secOut = checkTime(sec);
minOut = checkTime(min);
hourOut = checkTime(hour);
milisec = ++milisec;
if (milisec === 100) {
milisec = 0;
sec = ++sec;
}
if (sec == 60) {
min = ++min;
sec = 0;
}
if (min == 60) {
min = 0;
hour = ++hour;
}
document.getElementById("sec").innerHTML = secOut;
document.getElementById("min").innerHTML = minOut;
// document.getElementById("hour").innerHTML = hourOut;
}
/* Adds 0 when value is <10 */
function checkTime(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
function reset() {
/*Reset*/
milisec = 0;
sec = 0;
min = 0
hour = 0;
document.getElementById("milisec").innerHTML = "00";
document.getElementById("sec").innerHTML = "00";
document.getElementById("min").innerHTML = "00";
document.getElementById("hour").innerHTML = "00";
}
//adding an event listener
window.addEventListener('load', init, false);
]2]2
localStorage, as the name suggests is local - the data will only be accessible in the browser it's saved in. sessionStorage is also local. Generally anything that doesn't rely on a server will be local.
To store and display the data across multiple clients, you will need a server. What you need in this case is a simple back end that will provide two endpoints - one that will display the scoreboard (GET) and another that will save the data into the scoreboard (POST). There are many options you can use to create that, if you're familiar with node.js you could try express and sequelize with sqlite (if you need a database) or you could store the data in a text (CSV) file.
To retrieve the data from the web app you will need to use AJAX, if you want an easy way to make HTTP requests from JS try fetch - it's available in all modern browsers. Make sure to configure CORS properly on the server - this will cause some headaches in the beginning but a value of * for Access-Control-Allow-Origin is generally fine for projects like that.
The general flow would be like this:
After your web app is opened, the JS will fetch /scores (without any options).
Whenever there's a need to store a score you would fetch /scores/create with method: 'POST' and body: JSON.stringify(scoreObject).
Here's an example on how to get data from fetch:
async function getScores() {
try {
const res = await fetch('http://localhost:8080/scores');
const json = await res.json();
// json will contain your scores
} catch (e) {
// Something went wrong.
console.log(e); // Check console for more details.
}
}
It may sound extremely difficult at first but once you start working on this everything will start to make sense.
To reiterate:
Make a back end and host it somewhere. Make sure to configure CORS.
Use fetch to get your data to (and from) your page.
Keep on experimenting, that's the best way to learn.
You will need to host the site somewhere in order for multiple users to share application state. The "Local Storage" API's are out of the question, unfortunately.
Read up on web storage API's here: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
As soon as you have the need to persist data across multiple browsers and users you have now entered some fiery challenges. There's a thousand ways to solve this one. Some of the concerns to consider are:
Security: How will you be able to securely store each users' data and know which requests come from which user?
Storage: The data has to live somewhere outside of any individual users browser. Thus, you will need some sort of hosting.
What about when you need to update the quiz? What happens to old completed quizzes? Will they be schema-compatible? Data migration is... lovely.
I would suggest the following study:
Figure out a way to persist your web application data. This means you will need to either host or setup an account with a provider such as Amazon, Google or Microsoft Azure where you can host your data.
For simple testing you can use the AJAX API's (the fetch thing mentioned before), or it is worth spending some time studying up on modern frameworks such as Angular, React or Vue.
If you feel the momentum while grappling with AJAX or one of the web UI frameworks, then these links might help:
https://aws.amazon.com/serverless/build-a-web-app/
https://aws.amazon.com/websites/
https://firebase.google.com/docs/web/setup
https://azure.microsoft.com/en-us/services/app-service/web/
Ultimately there are a TON of options here... these are just a few approaches to solving the problem. You can setup your own VM, you can use a myriad of "serverless" options, you can create hosted "functions" with persistence... look around a bit.
The most important thing I would suggest learning is how data is stored in backend databases for web applications. Check out the free options for some of the major services. They almost all have an option for using their services for free as long as you don't exceed monthly thresholds. Even after those thresholds are met, it's really not that expensive to have a hosted web application with some sort of database back-end.
It may sound daunting but it might be worth considering grabbing a good book on building web applications. Something using .NET Core, Python or node.js makes a reasonable back-end for API code. It really comes down to whatever you're comfortable with.
Ask some specific questions on here once you latch on to something specific and I'm sure the community will be happy to help.
Good luck!
I'm working on this code :
JS
<script src='file://C:\blablabla\JavaScript\bignumber.js-master\bignumber.js'></script>
<script>
document.write("<h1>\"blabla\"</h1>\n<h3>blabla</h3>");
function problem(){
var img = document.getElementById('problemi');
return img.style.display = img.style.display === 'block' ? 'none' : 'block';
}
function problem551(){
problem();
var t0 = performance.now();
var max = 1e+15;
var sum = new BigNumber(1);
for(var i=1;i<max;i++)
sum = sum.plus(scomponi(sum,0));
var t1 = performance.now();
document.getElementById("p551").innerHTML = 'blabla<span>'+max+"</span> blabla <span>" + sum +"</span> in <span>"+(t1 - t0)/1000+"</span> blaaa";
}
function scomponi(num,sum){
var str=num.toString();
for(var i = 0 ; i< str.length ;i++ ){
sum += parseInt(str[i]);
}
return sum;
}
</script>
HTML
<body>
<div>
<button onclick="problem551()" >PROBLEM 551</button>
<img id="problemi" src="PROBLEM551.png" style="display: none;">
<p id="p551"></p>
</div>
</body>
But Chrome crashes, it gives me this :
How can prevent this error on my function, he has a loop from 1 to 1e+15, so it takes too much time. I read something about WEB WORKERS but is unclared for me. I want to use it on my function problem551(), so someone can explain me how it works?
Move your functions to a new file, funcs.js:
And use new Worker("funcs.js") to start it.
You can use postMessage to send the result back as described in MDN:
Sending messages to and from a dedicated worker
The magic of workers happens via the postMessage() method and the
onmessage event handler. When you want to send a message to the
worker, you post messages to it like this (main.js):
first.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
So here we have two elements represented by the variables
first and second; when the value of either is changed,
myWorker.postMessage([first.value,second.value]) is used to send the
value inside both to the worker, as an array. You can send pretty much
anything you like in the message.
In the worker, we can respond when the message is received by writing
an event handler block like this (worker.js):
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
Do expect it to still take a lot of time, it is quite a long operation, but it will hopefully prevent Chrome from bringing up the error.
I see you are trying to solve Project Euler problem 551.
You are throwing away a valuable piece of information:
You are given a_10^6 = 31054319.
You don't need to begin your iterations from 1. The next number can any arbitary number in the sequence.
a_10^6 = 31054319
a_(10^6 + 1) = 31054319 + 3 + 1 + 0 + 5 + 4 + 3 + 1 + 9
So I recently decided to make an idle game from AngularJS, but after a few days of coding it I realized a problem.
The $interval function slows down a huge amount when not actively looking at the screen.
Searching on Google and stackoverflow took me to web workers, and one post actually had me figure out how to make a factory of a web worker to implement into my controller, but I was never able to figure out how to make them run functions (or if this is even possible).
myApp.factory("interval", ['$q',
function($q) {
var blobURL = URL.createObjectURL(new Blob([
'function addInterval() {' +
'i = "add()";' +
'postMessage(i);' +
'setTimeout("addInterval()", 10);' +
'}' +
'addInterval();' +
'function saveInterval() {' +
'x = "save()";' +
'postMessage(x);' +
'setTimeout("saveInterval()", 30000);' +
'}' +
'saveInterval();'
], {
type: 'application/javascript'
}));
var worker = new Worker(blobURL);
var defer = $q.defer();
worker.addEventListener('message', function(e) {
defer.resolve(e.data);
}, false);
return {
doWork: function(myData) {
defer = $q.defer();
worker.postMessage(myData); // Send data to our worker.
return defer.promise;
}
};
}
]);
I practically want to know if it's possible to have the web workers run my save() and interval() functions that are in the controller. I tried to console.log(defer.resolve(e.data)), but it returned undefined.
...and I literally have no clue what the deferring/resolving/promising even does.
Thanks!
So after Scott corrected me about how web workers function I decided to set the interval from 10ms to 1000ms...and was defeated...
...but then I got the great idea of "How about instead of making it one fixed rate, I'll cancel the interval upon page/tab blur (not focussed) and change the intervals + divisions accordingly!". This is what I came up with that solved my problems and made the game continue to run - even when not viewing it.
$scope.division = 100;
var x = $interval(add, 10);
angular.element($window).bind('focus', function() {
$scope.division = 100;
$interval.cancel(x);
x = $interval(add, 10);
}).bind('blur', function() {
$scope.division = 1;
$interval.cancel(x);
x = $interval(add, 1000);
});
i'm developing a phonegap app using a lot of javascript. Now i'm debugging it using Safari Developer Tool, in particular i'm focused on some button that on the device seems to be a bit luggy.
So I've added some console.timeEnd() to better understand where the code slow down, but the "problem" is that when i open the console the code start running faster without lag, if i close it again, the lag is back.
Maybe my question is silly but i can't figure it out
Thanks
EDIT: Added the code
function scriviNumeroTastiera(tasto){
console.time('Funzione ScriviNumeroTastiera');
contenutoInput = document.getElementById('artInserito').value;
if ($('#cursoreImg').css('display') == 'none'){
//$('#cursoreImg').show();
}
else if (tasto == 'cancella'){
//alert(contenutoInput.length);
if (contenutoInput.length == 0) {
}
else {
indicePerTaglioStringa = (contenutoInput.length)-1;
contenutoInput = contenutoInput.substr(0, indicePerTaglioStringa);
$('#artInserito').val(contenutoInput);
//alert('tastoCanc');
margineAttualeImg = $('#cursoreImg').css('margin-left');
indicePerTaglioStringa = margineAttualeImg.indexOf('p');
margineAttualeImg = margineAttualeImg.substr(0, indicePerTaglioStringa);
margineAggiornato = parseInt(margineAttualeImg)-20;
$('#cursoreImg').css('margin-left', margineAggiornato+'px');
}
}
else {
//contenutoInput = document.getElementById('artInserito').value;
contenutoAggiornato = contenutoInput+tasto;
margineAttualeImg = $('#cursoreImg').css('margin-left');
indicePerTaglioStringa = margineAttualeImg.indexOf('p');
margineAttualeImg = margineAttualeImg.substr(0, indicePerTaglioStringa);
margineAggiornato = parseInt(margineAttualeImg)+20;
$('#cursoreImg').css('margin-left', margineAggiornato+'px');
$('#artInserito').val(contenutoAggiornato);
}
console.timeEnd('Funzione ScriviNumeroTastiera');
}
The code is a bit crappy, but it's just a beginning ;)
This could happen because PhoneGap/Cordova creates its own console object (in cordova.js), and it gets overwritten when you open the Safari console (safari's might be faster than phonegap's, that could be why you notice it faster).
So, one way to measure the time properly, without opening the console, would be to go to the good old alert, so you'd first add this code anywhere in your app:
var TIMER = {
start: function(name, reset){
if(!name) { return; }
var time = new Date().getTime();
if(!TIMER.stimeCounters) { TIMER.stimeCounters = {} };
var key = "KEY" + name.toString();
if(!reset && TIMER.stimeCounters[key]) { return; }
TIMER.stimeCounters[key] = time;
},
end: function(name){
var time = new Date().getTime();
if(!TIMER.stimeCounters) { return; }
var key = "KEY" + name.toString();
var timeCounter = TIMER.stimeCounters[key];
if(timeCounter) {
var diff = time - timeCounter;
var label = name + ": " + diff + "ms";
console.info(label);
delete TIMER.stimeCounters[key];
}
return diff;
}
};
(This just mimics the console.time and console.timeEnd methods, but it returns the value so we can alert it).
Then, instead of calling:
console.time('Funzione ScriviNumeroTastiera');
you'd call:
TIMER.start('Funzione ScriviNumeroTastiera');
and instead of calling:
console.timeEnd('Funzione ScriviNumeroTastiera');
you'd call:
var timeScriviNumeroTastiera = TIMER.end('Funzione ScriviNumeroTastiera');
alert('Ellapsed time: ' + timeScriviNumeroTastiera);
This would give you the proper ellapsed time without opening the console, so it computes the real time in the phonegap app.
Hope this helps.
Cheers
This really isn't something you would normally expect - opening the console should not speed up anything. If anything, it will make things slower because of additional debugging hooks and status display. However, I've had a case like that myself. The reason turned out to be very simple: opening the console makes the displayed portion of the website smaller and the code efficiency was largely dependent on the viewport size. So if I am right, making the browser window smaller should have the same effect as opening the console.
I have a button which runs a long running function when it's clicked. Now, while the function is running, I want to change the button text, but I'm having problems in some browsers like Firefox, IE.
html:
<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
javascript:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
Now this has problems in firefox and IE, ( in chrome it works ok )
So I thought to put it into a settimeout:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
but this doesn't work either for firefox! the button gets disabled, changes colour ( due to the application of the new css ) but the text does not change.
I have to set the time to 50ms instead of just 0ms, in order to make it work ( change the button text ). Now I find this stupid at least. I can understand if it would work with just a 0ms delay, but what would happen in a slower computer? maybe firefox would need 100ms there in the settimeout? it sounds rather stupid. I tried many times, 1ms, 10ms, 20ms...no it won't refresh it. only with 50ms.
So I followed the advice in this topic:
Forcing a DOM refresh in Internet explorer after javascript dom manipulation
so I tried:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
var a = document.getElementById("mybutt").offsetTop; //force refresh
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
but it doesn't work ( FIREFOX 21). Then i tried:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
var a = document.getElementById("mybutt").offsetTop; //force refresh
var b = document.getElementById("myspan").offsetTop; //force refresh
var c = document.getElementById("mybutt").clientHeight; //force refresh
var d = document.getElementById("myspan").clientHeight; //force refresh
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
I even tried clientHeight instead of offsetTop but nothing. the DOM does not get refreshed.
Can someone offer a reliable solution preferrably non-hacky ?
thanks in advance!
as suggested here i also tried
$('#parentOfElementToBeRedrawn').hide().show();
to no avail
Force DOM redraw/refresh on Chrome/Mac
TL;DR:
looking for a RELIABLE cross-browser method to have a forced DOM refresh WITHOUT the use of setTimeout (preferred solution due to different time intervals needed depending on the type of long running code, browser, computer speed and setTimeout requires anywhere from 50 to 100ms depending on situation)
jsfiddle: http://jsfiddle.net/WsmUh/5/
Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser. That means if you set some element.style.[...] = ... it won't kick in until your code finishes running (either completely, or because the browser sees you're doing something that lets it intercept processing for a few ms).
You have two problems: 1) your button has a <span> in it. Remove that, just set .innerHTML on the button itself. But this isn't the real problem of course. 2) you're running very long operations, and you should think very hard about why, and after answering the why, how:
If you're running a long computational job, cut it up into timeout callbacks (or, in 2019, await/async - see note at the end of this anser). Your examples don't show what your "long job" actually is (a spin loop doesn't count) but you have several options depending on the browsers you take, with one GIANT booknote: don't run long jobs in JavaScript, period. JavaScript is a single threaded environment by specification, so any operation you want to do should be able to complete in milliseconds. If it can't, you're literally doing something wrong.
If you need to calculate difficult things, offload it to the server with an AJAX operation (universal across browsers, often giving you a) faster processing for that operation and b) a good 30 seconds of time that you can asynchronously not-wait for the result to be returned) or use a webworker background thread (very much NOT universal).
If your calculation takes long but not absurdly so, refactor your code so that you perform parts, with timeout breathing space:
function doLongCalculation(callbackFunction) {
var partialResult = {};
// part of the work, filling partialResult
setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}
function doSecondBit(partialResult, callbackFunction) {
// more 'part of the work', filling partialResult
setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}
function finishUp(partialResult, callbackFunction) {
var result;
// do last bits, forming final result
callbackFunction(result);
}
A long calculation can almost always be refactored into several steps, either because you're performing several steps, or because you're running the same computation a million times, and can cut it up into batches. If you have (exaggerated) this:
var resuls = [];
for(var i=0; i<1000000; i++) {
// computation is performed here
if(...) results.push(...);
}
then you can trivially cut this up into a timeout-relaxed function with a callback
function runBatch(start, end, terminal, results, callback) {
var i;
for(var i=start; i<end; i++) {
// computation is performed here
if(...) results.push(...); }
if(i>=terminal) {
callback(results);
} else {
var inc = end-start;
setTimeout(function() {
runBatch(start+inc, end+inc, terminal, results, callback);
},10);
}
}
function dealWithResults(results) {
...
}
function doLongComputation() {
runBatch(0,1000,1000000,[],dealWithResults);
}
TL;DR: don't run long computations, but if you have to, make the server do the work for you and just use an asynchronous AJAX call. The server can do the work faster, and your page won't block.
The JS examples of how to deal with long computations in JS at the client are only here to explain how you might deal with this problem if you don't have the option to do AJAX calls, which 99.99% of the time will not be the case.
edit
also note that your bounty description is a classic case of The XY problem
2019 edit
In modern JS the await/async concept vastly improves upon timeout callbacks, so use those instead. Any await lets the browser know that it can safely run scheduled updates, so you write your code in a "structured as if it's synchronous" way, but you mark your functions as async, and then you await their output them whenever you call them:
async doLongCalculation() {
let firstbit = await doFirstBit();
let secondbit = await doSecondBit(firstbit);
let result = await finishUp(secondbit);
return result;
}
async doFirstBit() {
//...
}
async doSecondBit...
...
SOLVED IT!! No setTimeout()!!!
Tested in Chrome 27.0.1453, Firefox 21.0, Internet 9.0.8112
$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);
function longFunc(){
//Do your long running work here...
for (i = 1; i<1003332300; i++) {}
//And on finish....
$('#btn').html('done');
}
DEMO HERE!
As of 2019 one uses double requesAnimationFrame to skip a frame instead of creating a race condition using setTimeout.
function doRun() {
document.getElementById('app').innerHTML = 'Processing JS...';
requestAnimationFrame(() =>
requestAnimationFrame(function(){
//blocks render
confirm('Heavy load done')
document.getElementById('app').innerHTML = 'Processing JS... done';
}))
}
doRun()
<div id="app"></div>
As an usage example think of calculating pi using Monte Carlo in an endless loop:
using for loop to mock while(true) - as this breaks the page
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
for(let i = 0; i < 1000; i++){
// mocks
// while(true){
// basically last value will be rendered only
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
}
<div id="app"></div>
And now think about using requestAnimationFrame for updates in between ;)
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
function rAFLoop(calculate){
return new Promise(resolve => {
requestAnimationFrame( () => {
requestAnimationFrame(() => {
typeof calculate === "function" && calculate()
resolve()
})
})
})
}
let stopped = false
async function piDOM(){
while(stopped==false){
await rAFLoop(() => {
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
})
}
}
function stop(){
stopped = true;
}
function start(){
if(stopped){
stopped = false
piDOM()
}
}
piDOM()
<div id="app"></div>
<button onclick="stop()">Stop</button>
<button onclick="start()">start</button>
As described in the "Script taking too long and heavy jobs" section of Events and timing in-depth (an interesting reading, by the way):
[...] split the job into parts which get scheduled after each other. [...] Then there is a “free time” for the browser to respond between parts. It is can render and react on other events. Both the visitor and the browser are happy.
I am sure that there are many times in which a task cannot be splitted into smaller tasks, or fragments. But I am sure that there will be many other times in which this is possible too! :-)
Some refactoring is needed in the example provided. You could create a function to do a piece of the work you have to do. It could begin like this:
function doHeavyWork(start) {
var total = 1000000000;
var fragment = 1000000;
var end = start + fragment;
// Do heavy work
for (var i = start; i < end; i++) {
//
}
Once the work is finished, function should determine if next work piece must be done, or if execution has finished:
if (end == total) {
// If we reached the end, stop and change status
document.getElementById("btn").innerHTML = "done!";
} else {
// Otherwise, process next fragment
setTimeout(function() {
doHeavyWork(end);
}, 0);
}
}
Your main dowork() function would be like this:
function dowork() {
// Set "working" status
document.getElementById("btn").innerHTML = "working";
// Start heavy process
doHeavyWork(0);
}
Full working code at http://jsfiddle.net/WsmUh/19/ (seems to behave gently on Firefox).
If you don't want to use setTimeout then you are left with WebWorker - this will require HTML5 enabled browsers however.
This is one way you can use them -
Define your HTML and an inline script (you don't have to use inline script, you can just as well give an url to an existing separate JS file):
<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>
<script type="javascript/worker">
postMessage('Worker is ready...');
onmessage = function(e) {
if (e.data === 'start') {
//simulate heavy work..
var max = 500000000;
for (var i = 0; i < max; i++) {
if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
}
postMessage('Done!');
}
};
</script>
For the inline script we mark it with type javascript/worker.
In the regular Javascript file -
The function that converts the inline script to a Blob-url that can be passed to a WebWorker. Note that this might note work in IE and you will have to use a regular file:
function getInlineJS() {
var js = document.querySelector('[type="javascript/worker"]').textContent;
var blob = new Blob([js], {
"type": "text\/plain"
});
return URL.createObjectURL(blob);
}
Setup worker:
var ww = new Worker(getInlineJS());
Receive messages (or commands) from the WebWorker:
ww.onmessage = function (e) {
var msg = e.data;
document.getElementById('status').innerHTML = msg;
if (msg === 'Done!') {
alert('Next');
}
};
We kick off with a button-click in this demo:
document.getElementById('start').addEventListener('click', start, false);
function start() {
ww.postMessage('start');
}
Working example here:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/
As you can see the user-interface is updated (with progress in this example) even if we're using a busy-loop on the worker. This was tested with an Atom based (slow) computer.
If you don't want or can't use WebWorker you have to use setTimeout.
This is because this is the only way (beside from setInterval) that allow you to queue up an event. As you noticed you will need to give it a few milliseconds as this will give the UI engine "room to breeth" so-to-speak. As JS is single-threaded you cannot queue up events other ways (requestAnimationFrame will not work well in this context).
Hope this helps.
Update: I don't think in the long term that you can be sure of avoiding Firefox's aggressive avoidance of DOM updates without using a timeout. If you want to force a redraw / DOM update, there are tricks available, like adjusting the offset of elements, or doing hide() then show(), etc., but there is nothing very pretty available, and after a while when those tricks get abused and slow down user experience, then browsers get updated to ignore those tricks. See this article and the linked articles beside it for some examples: Force DOM redraw/refresh on Chrome/Mac
The other answers look like they have the basic elements needed, but I thought it would be worthwhile to mention that my practice is to wrap all interactive DOM-changing functions in a "dispatch" function which handles the necessary pauses needed to get around the fact that Firefox is extremely aggressive in avoiding DOM updates in order to score well on benchmarks (and to be responsive to users while browsing the internet).
I looked at your JSFiddle and customized a dispatch function the one that many of my programs rely on. I think it is self-explanatory, and you can just paste it into your existing JS Fiddle to see how it works:
$("#btn").on("click", function() { dispatch(this, dowork, 'working', 'done!'); });
function dispatch(me, work, working, done) {
/* work function, working message HTML string, done message HTML string */
/* only designed for a <button></button> element */
var pause = 50, old;
if (!me || me.tagName.toLowerCase() != 'button' || me.innerHTML == working) return;
old = me.innerHTML;
me.innerHTML = working;
setTimeout(function() {
work();
me.innerHTML = done;
setTimeout(function() { me.innerHTML = old; }, 1500);
}, pause);
}
function dowork() {
for (var i = 1; i<1000000000; i++) {
//
}
}
Note: the dispatching function also blocks calls from happening at the same time, because it can seriously confuse users if status updates from multiple clicks are happening together.
Fake an ajax request
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
$.ajax({
url: "/",
complete: function () {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
});}
Try this
function longRunningTask(){
// Do the task here
document.getElementById("mybutt").value = "done";
}
function longrunningfunction() {
document.getElementById("mybutt").value = "doing some work";
setTimeout(function() {
longRunningTask();
}, 1);
}
Some browsers don't handle onclick html attribute good. It's better to use that event on js object. Like this:
<button id="mybutt" class="buttonEnabled">
<span id="myspan">do some work</span>
</button>
<script type="text/javascript">
window.onload = function(){
butt = document.getElementById("mybutt");
span = document.getElementById("myspan");
butt.onclick = function () {
span.innerHTML = "doing some work";
butt.disabled = true;
butt.className = "buttonDisabled";
//long running task here
span.innerHTML = "done";
};
};
</script>
I made a fiddle with working example http://jsfiddle.net/BZWbH/2/
Have you tried adding listener to "onmousedown" to change the button text and click event for longrunning function.
Slightly modified your code at jsfiddle and:
$("#btn").on("click", dowork);
function dowork() {
document.getElementById("btn").innerHTML = "working";
setTimeout(function() {
for (var i = 1; i<1000000000; i++) {
//
}
document.getElementById("btn").innerHTML = "done!";
}, 100);
}
Timeout set to more reasonable value 100ms did the trick for me. Try it.
Try adjusting the latency to find the best value.
DOM buffer also exists in default browser on android,
long running javascript only flush DOM buffer once,
use setTimeout(..., 50) to solve it.
I have adapted Estradiaz's double animation frame method for async/await:
async function waitForDisplayUpdate() {
await waitForNextAnimationFrame();
await waitForNextAnimationFrame();
}
function waitForNextAnimationFrame() {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
async function main() {
const startTime = performance.now();
for (let i = 1; i <= 5; i++) {
setStatus("Step " + i);
await waitForDisplayUpdate();
wasteCpuTime(1000);
}
const elapsedTime = Math.round(performance.now() - startTime);
setStatus(`Completed in ${elapsedTime} ms`);
}
function wasteCpuTime(ms) {
const startTime = performance.now();
while (performance.now() < startTime + ms) {
if (Math.random() == 0) {
console.log("A very rare event has happened.");
}
}
}
function setStatus(s) {
document.getElementById("status").textContent = s;
}
document.addEventListener("DOMContentLoaded", main);
Status: <span id="status">Start</span>