Reputation: 6040
I'm trying to hide the text of a button and show a spinner in its place while processing something. However no styling is applied even though the classes get properly toggled to the right value as I see on the Developer Tools.
function toggleSpinner() {
$('button span').toggleClass('display-none');
$('button .fa-spinner').toggleClass('display-none');
}
function generateGrid(size) {
let arr = getRandomNumbers(size);
toggleSpinner(); // <--- HIDE TEXT SHOW SPINNER
$('.grid').empty();
let html = '';
arr.forEach((element) => {
html += '<div class="box">' + element.toLocaleString() + '</div>';
});
$('.grid').append(html);
toggleSpinner(); // <--- HIDE SPINNER SHOW TEXT
}
And this is my HTML:
<button>
<span>Generate Random Numbers</span>
<i class="fas fa-spinner fa-spin display-none"></i>
</button>
Generating the grid sometimes takes like 3 seconds depending on the size of the grid and during that time, I would like to show the spinner on the button instead of the text.
The first time toggleSpinner()
gets called, I expect the button to only display a spinning icon without the text. The second time toggleSpinner()
gets called, I expect the spinning icon to go away and the button's text to show up again.
What's happening is that even though the classes get toggled, it's like the browser isn't repainting the page until everything is done so in effect, it's like toggleSpinner()
gets called twice which results to the same classes from the beginning resulting in no changes to the button.
Upvotes: 0
Views: 87
Reputation: 19301
Changes to the DOM are not being rendered because generating the grid is blocking the event loop. Hence running generateGrid
asynchronously should solve the issue. However just running it as an asynchronous function and waiting for it to finish (using await
), or putting the code directly in a promise handler, risks putting processing in the microtask queue - which also blocks the event loop.
This example simulate "generating the grid" by simply running a loop for 3 seconds. It is called from a timer call with an additional callback argument to signal when it has done. The calling function, makeGrid
returns a promise this is fulfilled when processing has completed:
"use strict";
function toggleSpinner() {
$('button span').toggleClass('display-none');
$('button .fa-spinner').toggleClass('display-none');
}
function makeGrid(size) {
toggleSpinner(); // show spinner
return new Promise( resolve => {
setTimeout( generateGrid, 0, size, function() {
toggleSpinner(); // hide spinner
resolve();
});
});
}
function generateGrid( size, done) {
// dummied code just to run synchonously for 3 seconds
const end = Date.now() + 3000;
while( Date.now() < end);
done(); // callback when done
}
function test() {
console.log( "calling makeGrid");
makeGrid(1).then( ()=>console.log("makeGrid finished"));
}
.display-none {
display: none;
}
.fa-spinner:before {
content: "⏳";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js" crossorigin></script>
<!-- body-html -->
<button>
<span>Generate Random Numbers</span>
<i class="fas fa-spinner fa-spin display-none"></i>
</button>
<p>
<button onclick="test()">test</button> <-- click here to test
Upvotes: 1
Reputation: 9681
It's rudamentary and there are probably better ways but one option is to take the first toggleSpinner
out of the method and use a timeout, which then calls your generateGrid
function. This gives the CSS and class toggles time to do their thing before the render blocking of the generateGrid
happens.
function toggleSpinner() {
$('button span').toggleClass('display-none');
$('button .fa-spinner').toggleClass('display-none');
}
function getRandomNumbers(size) {
return new Array(size).fill('foo')
}
function generateGrid(size) {
let arr = getRandomNumbers(size);
$('.grid').empty();
let html = '';
arr.forEach((element) => {
html += '<div class="box">' + element.toLocaleString() + '</div>';
});
$('.grid').append(html);
toggleSpinner()
}
$('button').on('click', () => {
toggleSpinner()
setTimeout(() => { generateGrid(100000); }, 1000)
})
.display-none { display: none; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button>
<span>Generate Random Numbers</span>
<i class="fas fa-spinner fa-spin display-none">SPINNING</i>
</button>
<div class="grid"></div>
Upvotes: 0