Home > Back-end >  How to get an SVG spinner to show up before a heavy UI update?
How to get an SVG spinner to show up before a heavy UI update?

Time:10-14

I have a UI update in a Vue app where I update 10 apexcharts bar charts at once. It takes about one second for it to fully load, during which time I'd like to show an svg spinner. However, the normal pattern of having a v-if="showSpinner" on a div containing that spinner and setting showSpinner.value = true; for a const showSpinner = ref(false); isn't working. For example:

<template>
    <div v-if="showSpinner">
       [svg spinner]
    </div>
    <div>
       [10 apexcharts I'm updating at once with already existing local data]
    </div>
</template>

<script setup lang="ts">

import { ref, nextTick } from 'vue';

const showSpinner = ref(false);
const state = reactive({
    dataForCharts: {},
});

const someFuncThatGetsCalledMuchLater = () => {
    showSpinner.value = true;

    // I've also tried await nextTick(); (after making this
    // function itself async) but that didn't make any difference.
    nextTick(() => {
        // some code that modifies state.dataForCharts, causing the charts to redraw    
    });
}

</script>

Ignoring even the code that would have to exist to set showSpinner.value = false; later on, which is another problem I've yet to solve, this above code doesn't show the spinner until after all of the charts have updated (which basically freezes the webpage for 1s, since my understanding is that Javascript is single-threaded).

So my two questions are:

  1. How can I get this svg spinner to appear before the charts start updating and freeze the webpage?
  2. Not here yet since I haven't solved (1), but how can I listen for a callback for when the charts have finished updating? I don't think directives will work for me since it seems that they are supposed to only act on the element that they're attached to, and I don't think setting showSpinner.value = false; inside onUpdated(...) would work either since that is called after every reactive update, including the changing of showSpinner.value = true;, so setting it false there would just instantly undo the fact that we've just set it to true.

Edit: please see the comments to the accepted answer to see the answer to part (2).

CodePudding user response:

1. show spinner before charts

Basically you just want to update showSpinner first, then trigger a redraw, and then render the charts. The simplest way would be to use nextTick

import { ref, nextTick } from 'vue';

const someFuncThatGetsCalled = async () => {
  showSpinner.value = true;
  await nextTick(); // could also pass a callback
  state.dataForCharts = someDataAndNotNullAnyMore;
}

nextTick, according to documentation, should work:

A utility for waiting for the next DOM update flush.

Details

When you mutate reactive state in Vue, the resulting DOM updates are not applied synchronously. Instead, Vue buffers them until the "next tick" to ensure that each component updates only once no matter how many state changes you have made.

nextTick() can be used immediately after a state change to wait for the DOM updates to complete. You can either pass a callback as an argument, or await the returned Promise.

However in a simple test I've noticed it not do this correctly. There is a simple way to get around that though, and that is to use a setTimeout with value of 0.

const someFuncThatGetsCalled = () => {
  showSpinner.value = true;
  setTimeout(()=>{
    state.dataForCharts = someDataAndNotNullAnyMore;
  }, 0)
}

or with a utility function

async function nextTwik() {
  return new Promise((resolve) => setTimeout(() => resolve(), 0));
}

const someFuncThatGetsCalled = async () => {
  showSpinner.value = true;
  await nextTwik();
  state.dataForCharts = someDataAndNotNullAnyMore;
};

2. How to tell if a chart got mounted

apexcharts allow passing a configuration option where you can define the events

docs

async function nextTwik() {
  return new Promise((resolve) => setTimeout(() => resolve(), 0));
}
const chartsMounted = ref(0);

const chartOptions = {
  chart: {
    id: "bar-shart",
    events: {
      mounted(){
        chartsMounted.value   ;
      }
    },
    // animations, etc...
  },
  // dataLabels, plotOptions, xaxis, etc...
}

const someFuncThatGetsCalled = async () => {
  showSpinner.value = true;
  await nextTwik();
  // kick off chart rendering
  state.dataForCharts = someData;
}

watch(chartsMounted, (num) => {
  if (num === 10) {
    // all 10 charts loaded
    console.log("           
  • Related