Search code examples
javascriptvuejs3gif

How to show a GIF animation from the start every time condition is met - Vuejs 3 composition API


I have a component which is in charge of showing a .gif spinner every time a v-if="isLoading" condition is met.

The component (SpinnerLoader) looks like so:

<template>
  <div>
    <img :src="spinnerUrl" class="custom-size" />
  </div>
</template>

<script setup lang="ts">
import spinnerUrl from '@/assets/spinnerUrl.gif';
</script>

And I use it as a child component in another component template like so:

<template>
  <div>
    <SpinnerLoader v-if="isLoading" />
    <!-- some more code -->
  </div>
</template>

isLoading lives in the script of this parent component. And it is obviously a boolean.

The problem I have is that the .gif animation only starts from the beginning the first time the condition is met. After that, the animation just keeps going in the background and when the condition is met it does not show the animation from the start any more.

How can I modify the code so that the spinner .gif animation is shown from the start every time the condition is met ?

UPDATE

As mentioned by @MoritzRingler in the comments: this works properly in Firefox, but from some reason it does not in Chrome. Why would that be ? and how can it be solved ?


Solution

  • So this is browser related, it occurs in Chrome, but not in Firefox. I think I found a good solution, based on the recommended approach from this answer (it deserves more upvotes btw).

    The idea is to turn the gif into a data URL and then randomizing the URL every time the component loads. The advantage here is that the image does not have to be loaded every time, but it still looks like a different image to the browser.

    There is one caveat: If the image is hosted on a different server and does not have CORS enabled, this will fail (or need workarounds).

    Here is a link to a playground, might be easier to look at it there, but I'll add the code with some comments here for completeness.


    It's a composable that will give you a new URL every time its useRandomizedGifDataUrl() is called. So in your SpinnerLoader component, you just have to call the method and assign it to the src:

    <template>
        <img :src="src" class="custom-size" />
    </template>
    
    <script setup>
      import {useRandomizedGifDataUrl} from './loadingGif.js'
      const src = useRandomizedGifDataUrl()
    </script>
    

    And here it the composable:

    import {ref, watchEffect} from 'vue'
    const url = 'https://...'
    const dataUrl = ref('')
    
    /**
     * Loads an image and turns it into a data URL
     *  !!! this is sensitive to CORS !!!
     */
    const loadImageDataUrl = async () => {
        const blob = await fetch(url).then(res => res.blob())
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        const event = await new Promise(resolve => reader.onload = resolve)
        return event.currentTarget.result
    }
    
    /**
     * Provides access to the original dataUrl (empty on first call)
     */
    export function useLoadingGifDataUrl(){
      if (!dataUrl.value){
        loadImageDataUrl().then(imageUrl => {dataUrl.value = imageUrl})
      }
      return dataUrl
    }
    
    /**
     * Builds a random data URL based on the original dataUrl by 
     * adding a random number into the data header.
     */
    export function useRandomizedGifDataUrl(){
      const dataUrl = useLoadingGifDataUrl()
      const randomizedDataUrl = ref()
      const head = 'data:image/gif;base64'
      const randomize = (url) => !url ? '' : url.replace(head, `${head};${Math.random()}`)
      watchEffect(() => randomizedDataUrl.value = randomize(dataUrl.value))
      return randomizedDataUrl
    }
    

    This can be improved and tailored, for example by avoiding the watchEffect or adjusting behavior during the first load, but in general, this is how it seems to work.