Issue
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:
- How can I get this svg spinner to appear before the charts start updating and freeze the webpage?
- 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;
insideonUpdated(...)
would work either since that is called after every reactive update, including the changing ofshowSpinner.value = true;
, so setting it false there would just instantly undo the fact that we've just set it to true.
Solution
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 even though the DOM is updated, the browser may not re-render it. The subsequent js may block the rendering essentially rendering nextTick()
useless. To deal with this issue you need to manually defer initiating the js-heavy portion until the dom renders the initial change. You can use setTimeout
or requestAnimationFrame
to accomplish that. Using setTimeout
may work in some situations but is reported to behave inconsistently between browsers. It also can succumb similar issue as nextTick
and you may need to use a higher timeout than 0 to accommodate. using nested requestAnimationFrame
calls will wait for the browser to make the two renders before continuing, so, theoretically, is more reliable.
async function nextTwik(){
return new Promise((resolve)=>{
requestAnimationFrame(()=>{
requestAnimationFrame(()=>{
resolve()
})
})
})
}
then you can use as:
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
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(); // defined above
// kick off chart rendering
state.dataForCharts = someData;
}
watch(chartsMounted, (num) => {
if (num === 10) {
// all 10 charts loaded
console.log("👍great success👍");
showSpinner.value = false
}
})
As long as all charts have the mounted event listener that updates chartsMounted
this would notify you when all charts are loaded.
Update
The chart event functionality was tested with Vue2 and not Vue3. ATTOW, there is a bug in the vue3 version where this doesn't work. According to the responses, apexcharts support adding the event listeners to the component using @
, for example @mounted
. I'm leaving the options.events
based solution too since it reflects to the documentation.
const chartsMounted = ref(0);
function onChartMounted(){
chartsMounted.value ++;
}
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("👍great success👍");
showSpinner.value = false
}
})
template:
<apex-chart :options="options" :series="series" @mounted="onChartMounted" />
Answered By - Daniel Answer Checked By - Cary Denson (PHPFixing Admin)
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.