Loading spinner div not rendered in time - javascript

TL;DR:
loadingSpinner div toggled on before expensive code, toggled off after
both showLoading() and hideLoading() call log() which writes a message to console.log() and an element's innerHTML
the loadingSpinner and log message in the DOM do not show up before the expensive code is done but the console.log() messages show up when they should
I have a reference to a div stored in loadingSpinner which is just a box that sits above all the other content that should indicate that the site is doing some work. I use these functions to toggle visibility of said div (.hidden is just display: none; in my CSS)
function hideLoading() {
log('hiding')
loadingSpinner.style.display = 'none'
//setTimeout(function (){loadingSpinner.style.display = 'none'}, 10)
//window.getComputedStyle(loadingSpinner) // <-- TRIED FORCING REDRAW
//if (!loadingSpinner.classList.contains('hidden')) {
//loadingSpinner.classList.add('hidden')
//}
}
function showLoading(text) {
log('Showing')
loadingSpinner.innerHTML = text
loadingSpinner.style.display = 'block'
//setTimeout(function (){loadingSpinner.style.display = 'block'}, 10)
//window.getComputedStyle(loadingSpinner)
//if (loadingSpinner.classList.contains('hidden')) {
//loadingSpinner.classList.remove('hidden')
//}
}
function log(s) {
console.log(s)
logDisplay.innerText = s
}
The commented out code are different things I've tried already. The show and hide functions themselves work fine. I can tell that the hide and show functions are called at the right time because of the calls to log().
I have a few instances where the site does some expensive/long running tasks on the client of which nothing should be asynchronous, as far as I can tell (not sure about Array.prototype.forEach()). The Problem is that the loadingSpinner only shows up after the expensive task has completed and then hideLoading() hides it immediately. I did confirm this by adding a setTimeout() to hideLoading().
function updateDOM() {
showLoading('Updating DOM...') // <--- SHOW
log('Updating DOM') // <--- OTHER LOG MESSAGE
codeContainer.innerHTML = '' // <--- start of long running task
codes.forEach(code => {
if (code.base64 === '') {
backgroundQr.set({value: code.data})
code.base64 = backgroundQr.toDataURL()
}
addCodeElement(codeContainer, code)
});
if (codes.length === 0) {
editingId = -1
} // <--- end of long running task
hideLoading() // <--- HIDE
}
Console Log order is correct:
Showing
Updating DOM
hiding
But neither the text that log() writes to the logDisplay-Element nor the loadingSpinner itself show up when they should so I assume it is a rendering issue?
The issue is consistent in multiple places, not just the updateDOM() function.

As expensive code is being executed synchronously, the browser is too busy running it to find any time to render things to the DOM. One approach you can take is to execute expensive code asynchronously using promises and the setTimeout function to delay the expensive execution or send it to the end of the execution queue.
I've created the code snippet below that shows the approach, you'll need:
Spinner handling functions
Expensive executor function
Asynchronous code runner
Your main script that puts them all together
The snippet below contains two examples that you can toggle between, one performs a success execution, by running main();, the other a failure execution, by running main(true);.
function showSpinner() {
document.querySelector('#spinner').style.display = 'block';
}
function hideSpinner() {
document.querySelector('#spinner').style.display = 'none';
}
function executeSuccess() { // runs something expensive that succeeds
return 'data';
}
function executeFailure() { // runs something expensive that fails
throw 'issue';
}
function promiseToRunAsync(executor, ...params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try { resolve(executor(...params)); }
catch (error) { reject(error); }
}, 1000); // arbitrary time that you can set to anything including 0
});
}
function main(failure = false) {
showSpinner(); // show spinner
promiseToRunAsync(failure ? executeFailure : executeSuccess) // execute anync
.then((results) => {
console.log('yay', results);
document.querySelector('#content').innerHTML = results;
hideSpinner(); // hide spinner in case of success
})
.catch((error) => {
console.log('oops', error);
hideSpinner(); // hide spinner in case of failure
});
// ATTN code here will run before the spinner is hidden
}
main(); // executes the success scenario
// main(true); // executes the failure scenario
#spinner {
display: none;
}
<div id="spinner">Spinning...</div>
<div id="content"></div>
NOTE: In the example here, I am adding a 1 second delay to the execution, just to illustrate what's happening, but you'll probably need to set your own wait time or no wait time at all.

I think your problem is that the code is asynchronous.
In the second you start your forEach loop, the code continues all the way to hideLoading while the forEach loop is still executing, therefore you will never see the loader because you call showLoading and hideLoading right after each other.
Try changing your code like this:
function updateDOM() {
showLoading('Updating DOM...') // <--- SHOW
log('Updating DOM') // <--- OTHER LOG MESSAGE
codeContainer.innerHTML = '' // <--- start of long running task
for (const code of codes) {
if (code.base64 === '') {
backgroundQr.set({value: code.data})
code.base64 = backgroundQr.toDataURL()
}
addCodeElement(codeContainer, code)
}
if (codes.length === 0) {
editingId = -1
} // <--- end of long running task
hideLoading() // <--- HIDE
}

Related

How to stop custom function in JavaScript

look at this code
const code = "..." // can be anything, it is unknown
const func = new Function(code)
// run `func` and ignore possible errors in code
function run() {
try { func() } finally {}
}
// stop `func` execution
function stop() {
???
}
$runButton.onclick = run
$stopButton.onclick = stop
Is there any way to stop function execution
I just need to rerun function multiple times, kind of playground
But if code has timers, or infinite loops it freezes behaves badly
I don't want to reload playground site every time
Any ideas?
Same Situation in TS Playground, they don't care (press run multiple times)
Tried this, but that does not make sense
If that function's body is unpredictable
Also another way, which seems to be the most inefficient
Put checks in every line of function's body, and if flag is false break main label which is declared at the top of function's body for 'return' effect
let isRunning = true
code.split('/n').map(line => ';' + `if (!isRunning) break functionLabel` + ';' + line)
const functionBody = `functionLabel: { ${code} }`
const func = new Function(functionBody)
Create a web worker and pass code as message,
theoretically, it should run it in the background not affecting the main thread. Then terminate worker if you need to stop execution
For multiple 'reruns' reinit worker
worker.js
self.addEventListener('message', event => {
const { code } = event.data
const func = new Function(code)
try {
func()
self.postMessage('code ran successfully')
} catch (error) {
// handle possible error
self.postMessage('code ran with errors')
}
})
main.js
let worker = null
function run() {
let code = '...'
worker?.postMessage({ code })
}
function stop() {
if (worker == null) {
worker = new Worker(pathToWorker)
worker.addEventListener('message', event => {
console.log(event.data)
})
} else {
worker.terminate()
worker = null
}
}
Web Workers Support (98%)

Keep scrolling down while loading in cypress

I have a page that has an element where if you scroll down, it loads new data.
This takes about 10 seconds.
I have written the following test:
it('Should display at least one facility in booking panel', (done) => {
function recursivelyScroll() {
cy.get(element)
.scrollTo('bottom');
cy.get(element)
.then($el => {
// If the element contains a loading class, we wait one second and scroll down again
if ($el.find(Loading).length > 0) {
setTimeout(recursivelyScroll, 1000);
} else {
// We are done waiting, no more loading is needed
// write test here
done();
}
});
}
recursivelyScroll();
});
CypressError
Timed out after 4000ms. The done() callback was never invoked!
The done() method is not called fast enough according to Cypress, but they have no documentation on how to extend the done time period. Also, there might be a better way that I'm unaware of to create this scrollbehaviour in Cypress. Is there an easy fix?
Have you tried using the recursion plugin in cypress?
It would look something like this:
cy.get(element).scrollTo('bottom');
recurse(
() => cy.contains('element', 'Loading').should(() => {}),
($quote) => $quote.length > 0 ,
{
limit: 300,
log: false,
post(){
setTimeout(recursivelyScroll, 1000);
}
},
)
The idea was taken here: https://www.youtube.com/watch?v=KHn7647xOz8
Here example how to use and install https://github.com/bahmutov/cypress-recurse
Try to switch to When assertion and declare that function outside the execution:
When('Should display at least one facility in booking panel', (done) => {
recursivelyScroll(done)
}
function recursivelyScroll(done){
//see implementation below
}
Not sure how about which "Loading" is, maybe you have to put into double quotes?
Moreover please note that cypress is asynchronous, then the scrollTo and the then code sections are executed at same time in your code, just use then after the scroll.
And I will change the setTimeout into cy wait function before executing recursion, give a try to below code:
function recursivelyScroll(done) {
cy.get(element)
.scrollTo('bottom')
.then($el => {
// If the element contains a loading class, we wait one second and scroll down again
if ($el.find(".Loading").length > 0) {
cy.wait(1000).then(()=>{
recursivelyScroll(done);
})
} else {
// We are done waiting, no more loading is needed
// write test here
done();
}
});
}

How to stop function execution inside nested promises

I'm a beginner when it comes to promises and I'm trying to understand how to work with them.
I have a firebase trigger where I am performing some validation. If the validation fails, I want to "exit" the trigger, meaning I don't want any code after the validation to execute (assuming the validation failed). But it does. Even though I'm sure that the validation fails (the "You have been timed out due to inactivity. Please go back to the bed booking map and start again" is sent to the android app I'm developing), the code after keeps executing. I know this because I've placed console logs inside it.
I've put comments in my code for the validation I'm talking about, and what code I don't want executed.
exports.createCharge = functions.database.ref('/customers/{userId}/charges/{id}')
.onCreate((snap, context) => {
console.log("Entered createharge function");
const val = snap.val();
return admin.database().ref(`/customers/${context.params.userId}/customer_id`)
.once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
// Do stuff
if (val.type === 'beds') {
// Check if user email is the same as bed email
for (var i = 0; i < val.beds.length; i++) {
var bedEmailRef = db.ref(`beds/${val.hid}/${val.beds[i]}/email`);
bedEmailRef.on("value", function(bedEmailSnap) {
var bedEmail = bedEmailSnap.val();
if (val.email !== bedEmail) { // VALIDATION
snap.ref.child('error').set("You have been timed out due to inactivity. Please go back to the bed booking map and start again");
return null; // Here, I want to exit the whole function.
}
});
}
// If val.email !== bedEmail, I NEVER want to reach here!
return admin.database().ref(`/hotels/${val.hid}/bedPrice`)
.once('value').then((tempBedPrice) => {
// do stuff
console.log("got here");
return stripe.charges.create(charge, {idempotency_key: idempotencyKey});
}).then((response) => {
// Do more stuff
return snap.ref.set(response);
}).catch((error) => {
snap.ref.child('error').set(userFacingMessage(error));
return reportError(error, {user: context.params.userId});
})
} else throw Error('No type');
});
});
Why am I getting this behaviour? How can I stop the code after the validation from executing?
The problem is that you are adding a "listener" after checking for "beds" type. A solution would be to fetch data once:
// This is a snippet from Firebase documentation
return firebase.database().ref('/users/' +
userId).once('value').then(function(snapshot) {
var username = (snapshot.val() && snapshot.val().username) ||
'Anonymous';
// ...
});
Or refactor your code so your validation can be set as a callback that can be returned if some criteria is met.
Additionally, here's a link for Cloud functions best practices that can help you writing better cloud functions.
In cases where you want to exit / don't want anymore events on the listeners, refactor your code as below:
bedEmailRef.on("value", function(bedEmailSnap) {
var bedEmail = bedEmailSnap.val();
if (val.email !== bedEmail) { // VALIDATION
snap.ref.child('error').set("You have been timed out due to inactivity. Please go back to the bed booking map and start again");
bedEmailRef.off('value'); // This will stop listening for further events
return null; // This is useless because returning inside a listener has no effect
}
});

Two recursive calls in promise failing

I just wrote:
function getQtyFor(state,down) {
var promise = new RSVP.Promise( function (fulfill, reject) {
if (!down && (state=='bottle' || state=='pumped')) {
doDialog('volume')
.then ( validateVolume ,
function () { hideDialog('volume');
return getQtyFor(state,down); } )
.then ( function (q) { hideDialog('volume');
fulfill(q); },
function (e) { hideDialog('volume');
alert("Bad input "+e);
return getQtyFor(state,down); } )
} else fulfill(0);
});
return promise;
}
in which you can see two recursive calls. I'm finding that the first works as expected, and the second goes round in circles as expected, but I never get a 'fulfillment' after the second is used. In other words, I can run around in circles either by cancelling the dialog (first recursion) or by entering values that the validator doesn't like (second recursion.) In the former case, after I finally decide to hit OK, the waiting "then" will run and drop stuff in my database. But if I go round in circles by entering silly values and then finally enter a sensible value, then the waiting "then" will not run, and neither will it's rejection handler.
I kinda suspect the problem is that I'm fulfilling a different promise than the one with my handlers attached, but I still don't see why it works one way and not the other.
I guess you'll need the rest of the code to reproduce this so it's at http://quorve.com/rsvpissue/babylog.html. Here's the aforementioned waiting "then":
function stick(down) {
return function(ob) {
return getQtyFor(ob.attr('id'), down)
.then( getStickyFields(down, ob), alert ) //NEITHER REACHED AFTER FAILED VALIDATE
.then( db_insert('transitions') )
.then( function() { updateButtonAppearance(down)(ob); } , alert);
}
}
and the stuff called from the problematic recursive function:
function doDialog(log) {
var promise = new RSVP.Promise( function(fulfill, reject) {
$('#'+log).dialog({
dialogClass: "no-close",
modal: true,
buttons: {
"OK": function() { fulfill($(this)); },
"Cancel": function() { reject(); }
}
});
});
return promise;
}
function validateVolume(form) {
vol = form.find("[name=qty]").val();
if ( vol > 200 ) {
throw("vol"); }
return vol;
}
Steps to reproduce:
Use Chrome cos it uses WebSQL. (I know it's depped, but let's not change the subject.)
Go to http://quorve.com/rsvpissue/babylog.html
Click Zach's bottle (depressed button) and hit cancel in the dialog. The dialog persists until you hit OK, after which, you see a new transition bottom right representing the end of the drinking session with a quantity of 150ml. You were using the first recursive call when you were pressing cancel.
Reload the page (which resets the DB.) Click the bottle as before but push the slider to the top (no baby can drink that much) and press OK. Dismiss the nag box and the dialog reappears. Repeat if you like. Push the slider to a lowish value and press OK. Now you see the problem: the dialog disappears, so we've dropped out of the recursion, but the button and DB are not updated because the continuation is not called and neither is its rejection case.
Is this a bug in RSVP or have I misunderstood promises?
TIA, Adrian.
Sussed it! I should say:
fulfill(getQtyFor(state,down))
rather than
return getQtyFor(state,down)
for the recursive call.

JSON and Python

I am developing a web interface for Arduino, using Python. For automatic updates and display, I use JSON. I have a very interesting problem.
The following code sends the command to a python function, if a command exists. Then, whether a command was sent to the function or not, the function checks for updates from the Arduino by calling another function.
Here is what I can't find any explanation to: in the first and only condition of the update() function, if I remove the line that says alert('hey'); the python function is not called anymore. But if I do write alert('hey'); after the JSON request, it works fine, the function is called and the arduino gets the message.
Anyone has an idea why?
function update(command=0) {
// if a command is passed, send it
if (command!=0) {
$.getJSON('/action?command='+command);
alert('hey'); // if I remove this, the action function is not called. Why?
}
// read from the read function, no matter what
$.getJSON('/read', {}, function(data) {
if (data.state != 'failure' && data.content != '') {
$('.notice').text(data.content);
$('.notice').hide().fadeIn('slow');
setTimeout(function () { $('.notice').fadeOut(1000); }, 1500);
}
setTimeout(update, 5000); // next update in 5 secs
});
}
update(); // for the first call on page load
Are you checking for the results of the first command with the second? If so, I suspect the alert('hey') is pausing execution long enough for the first to finish. Can you try making your read a callback function of the first getJSON?
function update(command=0) {
if (command!=0) {
$.getJSON('/action?command='+command, function() {
read();
});
} else {
read();
}
}
function read() {
$.getJSON('/read', {}, function(data) {
if (data.state != 'failure' && data.content != '') {
$('.notice').text(data.content);
$('.notice').hide().fadeIn('slow');
setTimeout(function () { $('.notice').fadeOut(1000); }, 1500);
}
setTimeout(update, 5000); // next update in 5 secs
});
}
update(); // for the first call on page load
Here's a fiddle

Categories

Resources