3

I would like to display a loading spinner but ONLY if "my_code_here" takes longer than X milliseconds to execute.

I have managed to do this:

<button onClick='my_long_function()'></button>
<div class="spinner" style="display: none;"></div>
my_long_function: function()
{
    $(".spinner").show(200, function()
    {
        my_code_here
        my_code_here
        my_code_here

        $(".spinner").hide();
    }
}

It works but it shows the spinner even when the code takes less than 100ms, so it's showing up during the blink of an eye and I have issues making that look good.

Note that it's not ajax waiting for a server answer, it's all client side.

I tried stuff with setTimeout and clearTimeOut by reading like 5 stackoverflow questions but either it doesn't seem to trigger or the spinner stays forever.

Barbz_YHOOL
  • 589
  • 4
  • 15
  • 1
    Since javascript is single threaded I think the only way you could do this is run that function in a web worker which has it's own thread and use a setTimeout. – charlietfl Mar 16 '21 at 02:24
  • 2
    I'm confused, you want to show the spinner after x milliseconds, right? So `let timeout = setTimeout(showSpinner, x);` Then in your long running code, at the end, `clearTimeout(timeout); hideSpinner();`. It doesn't matter to `clearTimeout` if `timeout` has already elapsed, and if `showSpinner` and `hideSpinner` are idempotent (i.e., running hideSpinner on a hidden spinner doesn't do anything), you should be good. – Heretic Monkey Mar 16 '21 at 02:24
  • 1
    If it's all js, then the only way would be to have something in your `my_code_here` that triggers the display (eg after x iterations or comparing `current_time-start-time > timeout` - *within* your code. – freedomn-m Mar 16 '21 at 05:42
  • Your code, as-is will animate the show over 200ms *then* start your `my_code_here`. Change the timeout to something like (eg 2000) and put something simple as `my_code` (eg `console.log("start")`) and you'll see what's going on. – freedomn-m Mar 16 '21 at 05:44
  • @HereticMonkey I kept doing that and it just never showed the spinner (i think it probably appeared and disappeared in an instant). – Barbz_YHOOL Mar 16 '21 at 18:38
  • @freedomn-m counting the time elapsed may be a way but it's a loop, i'd need to check on each looped item, but that's an idea, ty – Barbz_YHOOL Mar 16 '21 at 18:39
  • floodlitworld's answer suggests an alternative suggestion: use css keyframes to wait before making visible - css-based animations don't use the same js thread so will continue to run. The idea is your js simply adds a class at the start and removes it at the end. The css then uses keyframes to wait then fadein after x seconds. Here's an existing solution that does just this: https://stackoverflow.com/a/29846733/2181514 – freedomn-m Mar 16 '21 at 18:45

2 Answers2

1

https://jsfiddle.net/w9khvx50/1/

const loader = document.querySelector('.loader');
const status = document.querySelector('.status');

// This function simply emulates an asynchronous function
const wait = (ms = 5000) => {
    return new Promise((resolve) => {
    setTimeout(() => {
        resolve();
    },ms)
  })
}

const toggleLoader = (state) => {
    loader.classList.toggle('show', state);
}


const timeout = setTimeout(toggleLoader, 2000);

status.textContent = "Starting load function";
wait(4000)
.then(() => {
    status.textContent = "Load function complete";
    toggleLoader(false);
        clearTimeout(timeout);
})
.loader {
  border: 16px solid #f3f3f3;
  border-radius: 50%;
  border-top: 16px solid #3498db;
  width: 120px;
  height: 120px;
  animation: spin 2s linear infinite;
  opacity: 0;
}

.loader.show {
  opacity: 1;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
<div>
  <div class="loader"></div>
  <div class="status"></div>
</div>

The best way here is the cleared timeout, I think you must have been implementing it wrongly before. If the ms in const timeout is less than the wait time, the loader will show.

Note that if the function you're executing is not asynchronous, then you'll need to investigate Web Workers, since your function will block the main thread and nothing else will execute (such as running a timeout callback). You should create a web worker, delegate your long function to it, and then clear the timeout if you receive a complete message before the time is up.

floodlitworld
  • 583
  • 4
  • 11
  • this is really different than my current code, i'm a bit lost but thank you, will analyze this (also checked a bit about web workers) – Barbz_YHOOL Mar 16 '21 at 18:40
0

In the past, I've done this with pure CSS. Building off of floodlitworld's answer, this uses CSS animations instead of JS to hide the spinner before showing it, with a more graceful transition to boot (timings exaggerated for emphasis):

const loader = document.querySelector('.loader');
const status = document.querySelector('.status');

// This function simply emulates an asynchronous function
const wait = (ms) => {
    return new Promise((resolve) => {
    setTimeout(() => {
        resolve();
    },ms)
  })
}

const toggleLoader = (state) => {
    loader.classList.toggle('show', state);
}


const makeCall = (s) => {
  toggleLoader(true);
  status.textContent = "Starting load function";
  wait(s * 1000)
  .then(() => {
      status.textContent = "Load function complete";
      toggleLoader(false);
  })
}
.loader {
  border: 16px solid #f3f3f3;
  border-radius: 50%;
  border-top: 16px solid #3498db;
  width: 0px;
  height: 0px;
  opacity: 0;
  animation: spin 2s linear infinite;
  box-sizing: border-box;
  margin-bottom: .5em;
}

.loader.show {
  opacity: 1;
  width: 120px;
  height: 120px;
  animation: spin 2s linear infinite, fadeIn 1.5s ease-out;
}

@keyframes fadeIn {
  0% {
    opacity: 0;
    width: 0px;
    height: 0px;
  }

  67% {
    opacity: 0;
    width: 0px;
    height: 0px;
  }

  100% {
    opacity: 1;
    width: 120px;
    height: 120px;
  }
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
<div>
  <div class="loader"></div>
  <button onClick="makeCall(1)">Make Fast Call</button>
  <button onClick="makeCall(7)">Make Slow Call</button>
  <div class="status"></div>
</div>
LightBender
  • 4,046
  • 1
  • 15
  • 31