KaptajnKold
KaptajnKold

Reputation: 10946

Don't update component until image loaded

I have an CurrentItem component, which shows an image and a description of some item. The item regularly changes based on events from the server. The problem I'm having is that when it does, the new item's image takes a few moments to load during which time the component just has a big blank space where the image is supposed to go, which does not look good.

What I would prefer instead is to keep showing the old item until the new item's image is loaded, and only at that point update the component. Is there a simple way to achieve that?

Some example code:

For the purposes of the question, you can think of an item as a simple JSON structure:

{
  "id": "ABC123",
  "description": "A fine yellow specimen",
  "image": "//example.com/images/ABC123.jpeg"
}

The events from the server JSON over a WebSocket, and look something like this:

{ "nextItemId": "ABC124" } 

When we receive one of those, we send a GET request to fetch the next item at //example.com/items/ABC124.json.

Upvotes: 1

Views: 926

Answers (3)

Dan
Dan

Reputation: 63059

Preload the image and wait for its onload event to fire before setting the item:

methods: {
   onReceived(newItem) {
      const image = new Image();  // Create new empty image
      image.onload = () => {      // CALLBACK:  Will fire when image is done loading
         this.item = newItem;         // Will set the current item
         this.isLoading = false;      // Will stop animation
      }
      this.isLoading = true;      // Start animation
      image.src = newItem.image;  // Begin loading 
   }
}

Once it's finished, the current item will be set. Here's a demo with a large image:

Vue.component('item', {
  props: ['item'],
  template: `
  <div>
    <div>ITEM (id: {{ item.id }})</div>
    <div>{{ item.description }}</div>
    <div><img :src="item.image" ref="img" width="100" height="100" /></div>
  </div>
  `
})

new Vue({
  el: "#app",
  data() {
    return {
      isLoading: false,
      item: {
        "id": "ABC123",
        "description": "A fine yellow specimen",
        "image": "https://cdn.iconscout.com/icon/free/png-256/google-470-675827.png"
      }
    }
  },
  methods: {
    simulateEvent() {
      this.onReceived({
        "id": "ZYX987",
        "description":  "A funky jaundiced specimen",
        "image": "https://upload.wikimedia.org/wikipedia/commons/f/fa/Very_Large_Array_in_New_Mexico.jpg"
      });
    },
    onReceived(newItem) {
      this.isLoading = true;
      const image = new Image();
      image.onload = () => {
        this.item = newItem;
        this.isLoading = false;
      }
      image.src = newItem.image;
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <item :item="item"></item>
  
  <!-- Simulating events -->
  <button @click="simulateEvent">
  Simulate event
  </button>
  <span v-if="isLoading">[ Loading... ]</span>
</div>

Upvotes: 2

Mohamed Raza
Mohamed Raza

Reputation: 953

you can set a time out function until the image loads

 .. codes to load new image
 setTimeout(function() {
      ... your old image codes
      ... 5000 means 5Seconds
  },5000);
.. codes to display new image

Upvotes: -1

KaptajnKold
KaptajnKold

Reputation: 10946

This is my own best shot at a solution.

Basically my idea is to have another copy of <CurrentItem /> in the DOM which has display:none; and which holds the item that is about to be the current. Then, when the image in that component is loaded, we update our actual CurrentItem. Something like this:

<template>
  <CurrentItem
    :key="currentItemId"
    :item="currentItem"
  />
  <CurrentItem
    :key="aboutToBecomeCurrentItemId"
    :item="aboutToBecomeCurrentItem"
    @imageLoaded="onImageReady"
    style="display:none;"
  />
</template>

<script>
// ...

export default {
  // ...

  data() {
    return {
      currentItem = null,
      aboutToBecomeCurrentItem = null
    },

    methods: {
      onItemUpdated(newItem) {
        this.aboutToBecomeCurrentItem = newItem;
      },
  
      onImageReady() {
        this.currentItem = this.aboutToBecomeCurrentItem;
      }
    }
  }

  // ...
}
</script>

I think this would work, but I would love to know it if there are simpler or more idiomatic ways to achieve the same.

Upvotes: 1

Related Questions