8

I created this component that fades in an image once it is loaded to the client. I would think there is a more Vue-like way to solve this, like using Vue events, but could not find it. What is the Vue way to detect when an image is loaded?

https://codepen.io/kslstn/pen/ooaPGW

Vue.component('imageThatFadesInOnLoad',{

  data: function(){
    return {
      src: 'http://via.placeholder.com/350x150',
      loaded: false,
    }
  },

  mounted: function () {
    var image = new Image()
    var that =  this
    this.loaded = image.addEventListener('load', function(){that.onLoaded()}) // This is the key part: it is basically vanilla JS
    image.src = this.src
  },

  methods:{
    onLoaded(){
      this.loaded = true
    }
  },

  template: `
    <div class="wrapper">
      <transition name="fade">
        <img class="icon" v-bind:src="src" v-if="loaded">&nbsp;
      </transition>
   </div>
  `
})

new Vue({
  el: '#wrapper'
});
.wrapper{
  width: 350px;
  height: 150px;
  background: slategrey;
}
.fade-enter-active {
  transition: opacity 3s ease-in-out;
}
.fade-enter-to{
  opacity: 1;
}
.fade-enter{
  opacity: 0;
}
<script src="https://unpkg.com/vue@2.4.4/dist/vue.js"></script>
<div id="wrapper">
<image-that-fades-in-on-load></image-that-fades-in-on-load>
</div>
kslstn
  • 1,407
  • 1
  • 14
  • 34
  • `@load="onLoaded"`? (or `v-on:load="onLoaded"`) https://jsfiddle.net/vcL986Lx/ – yuriy636 Nov 28 '17 at 15:46
  • Somehow that doesn't work in a template: https://codepen.io/kslstn/pen/dZgqNW I can't find documentation on v-on:load, but maybe it is called when the Vue app starts to load? At that time the template wouldn't be done rendering yet. – kslstn Nov 29 '17 at 08:22

3 Answers3

15

You can use the v-on: (or @ shorthand) syntax for binding to any DOM event. In your case the load event, which is triggered when the image is loaded.

Because you are loading the image "the DOM way" you can't use v-if because then Vue will not render the element (but you need it to, so the image src is fetched). Instead you can use v-show, which will render but hide the element.

Vue.component('imageThatFadesInOnLoad', {
  data: function() {
    return {
      src: 'http://via.placeholder.com/350x150',
      loaded: false,
    }
  },
  methods: {
    onLoaded() {
      this.loaded = true;
    }
  },
  template: `
    <div class="wrapper">
      <transition name="fade">
        <img class="icon" v-bind:src="src" v-on:load="onLoaded" v-show="loaded">&nbsp;
      </transition>
   </div>
  `
});

new Vue({
  el: '#wrapper'
});
.wrapper {
  width: 350px;
  height: 150px;
  background: slategrey;
}

.fade-enter-active {
  transition: opacity 3s ease-in-out;
}

.fade-enter-to {
  opacity: 1;
}

.fade-enter {
  opacity: 0;
}
<script src="https://unpkg.com/vue@2.4.4/dist/vue.js"></script>

<div id="wrapper">
  <image-that-fades-in-on-load></image-that-fades-in-on-load>
</div>
tony19
  • 125,647
  • 18
  • 229
  • 307
yuriy636
  • 11,171
  • 5
  • 37
  • 42
  • Nice! Thank you. I've updated my previously broken codepen: https://codepen.io/kslstn/pen/dZgqNW I've removed mounted and methods, as they are no longer necessary this way! – kslstn Nov 29 '17 at 21:08
  • 2
    I think this only works on first load. At least in my case. Because the @load event is not triggered if the image is already cached. Without the @load event `this.loaded` will never be set to true hiding my image forever... – I have not found a proper solution so far though. – but I also see that your codepen is working... . My code does not ‍♂️ – Merc May 09 '19 at 14:59
  • @Merc huh, never heard about this, but it looks legit. You may want to check for vanilla solutions in order to tweak Vue's code, like https://stackoverflow.com/questions/12354865/image-onload-event-and-browser-cache or http://mikefowler.me/journal/2014/04/22/cached-images-load-event | The promising solution looks like the one along "set event listener then src" – yuriy636 May 09 '19 at 20:21
  • thanks @yuriy636. I think I already found and read all your three pages. None of them seemed to solve my problem. (Although I have to say, that maybe I did not fully commit to trying out everything,...) . – Merc May 14 '19 at 23:29
1

Just to provide a slightly different approach to the answers above, using a native CSS solution instead of the Vue <Transition> way.

In the code below, we just add a class to the <img> when it loads triggering a transition.

<template>
  <img
    :class="imgIsLoaded ? 'show' : ''"
    src="myImg.jpg"
    loading="lazy"
    @load="imgLoadedMethod"
  >
</template>
<script>
export default {
  data () {
    return {
      imgIsLoaded: false
    }
  },
  methods: {
    imgLoadedMethod () {
      this.imgIsLoaded = true
    }
  }
}
</script>
<style scoped>
img {
  opacity: 0;
  transition: 3s;
}

img.show {
  opacity: 1;
}
</style>
Leopold Kristjansson
  • 2,246
  • 28
  • 47
  • how would this work with multiple images or elements from a loop? Trying to run a method on the element which returns a landscape or portrait class. – v3nt Mar 11 '21 at 19:28
  • @v3nt The code in the answer could be implemented as a component. You could then loop that component in the parent. – Leopold Kristjansson Mar 17 '21 at 08:44
0

To make transitions work when you dynamically replace cached images, you need to add a unique key. Otherwise vue's compiler will only replace the content of the element for efficiency and the transition will not be triggered.

<template>
  <div :style="{width: width+'px', height: height+'px'}">
    <transition name="fade">
      <img
        v-show="loaded"
        @load="onImageLoad"
        :src="src"
        :alt="alt"
        :width="width"
        :height="height"
        :key="src"
      />
    </transition>
  </div>
</template>
<script>
export default {
  props: ["src", "width", "height", "alt"],
  data() {
    return {
      loaded: false,
    };
  },
  methods: {
    onImageLoad() {
      this.loaded = true;
    },
  },
  watch: {
    src: {
      handler() {
        this.loaded = false;
      },
      immediate: true,
    },
  },
};
</script>
<style scoped>
.fade-enter-active {
  transition: opacity 1s ease-in-out;
}
.fade-enter-to {
  opacity: 1;
}
.fade-enter {
  opacity: 0;
}
</style>
Matthias
  • 1
  • 1