3

I am using Vue 3 and what i would like to achieve is to load all images inside a card (Album Card) and only then show the component on screen.. below is an how it looks now and also my code.

Does anybody have an idea how to achieve this?

currently component is shown first and then the images are loaded, which does not seem like a perfect user experience. example

<template>
  <div class="content-container">
    <div v-if="isLoading" style="width: 100%">LOADING</div>
    <album-card
      v-for="album in this.albums"
      :key="album.id"
      :albumTitle="album.title"
      :albumId="album.id"
      :albumPhotos="album.thumbnailPhotos.map((photo) => photo)"
    ></album-card>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import albumCard from "@/components/AlbumCard.vue";

interface Album {
  userId: number;
  id: number;
  title: string;
  thumbnailPhotos: Array<Photo>;
}

interface Photo {
  albumId: number;
  id: number;
  title: string;
  url: string;
  thumbnailUrl: string;
}

export default defineComponent({
  name: "Albums",
  components: {
    albumCard,
  },
  data() {
    return {
      albums: [] as Album[],
      isLoading: false as Boolean,
    };
  },
  methods: {
    async getAlbums() {
      this.isLoading = true;
      let id_param = this.$route.params.id;
      fetch(
        `https://jsonplaceholder.typicode.com/albums/${
          id_param === undefined ? "" : "?userId=" + id_param
        }`
      )
        .then((response) => response.json())
        .then((response: Album[]) => {
          //api returns array, loop needed
          response.forEach((album: Album) => {
            this.getRandomPhotos(album.id).then((response: Photo[]) => {
              album.thumbnailPhotos = response;
              this.albums.push(album);
            });
          });
        })
        .then(() => {
          this.isLoading = false;
        });
    },
    getRandomPhotos(albumId: number): Promise<Photo[]> {
      var promise = fetch(
        `https://jsonplaceholder.typicode.com/photos?albumId=${albumId}`
      )
        .then((response) => response.json())
        .then((response: Photo[]) => {
          const shuffled = this.shuffleArray(response);
          return shuffled.splice(0, 3);
        });

      return promise;
    },
    /*
     Durstenfeld shuffle by stackoverflow answer: 
     https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array/12646864#12646864
    */
    shuffleArray(array: Photo[]): Photo[] {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    },
  },
  created: function () {
    this.getAlbums();
  },
});
</script>
jckob
  • 41
  • 3
  • [Suspense](https://stackoverflow.com/questions/65774063/what-is-a-suspensible-component-in-vue-3) is probably what you looking for... – Michal Levý Aug 18 '21 at 11:21
  • @MichalLevý thank you for suggestion, but i solved this using different approach, see answer – jckob Aug 26 '21 at 11:51

1 Answers1

1

What i did to solve this problem was using function on load event on (img) html tag inside album-card component. While images are loading a loading spinner is shown. After three images are loaded show the component on screen.

<template>
  <router-link
    class="router-link"
    @click="selectAlbum(albumsId)"
    :to="{ name: 'Photos', params: { albumId: albumsId } }"
  >
    <div class="album-card-container" v-show="this.numLoaded == 3">
      <div class="photos-container">
        <img
          v-for="photo in this.thumbnailPhotos()"
          :key="photo.id"
          :src="photo.thumbnailUrl"
          @load="loaded()"
        />
      </div>
      <span>
        {{ albumTitle }}
      </span>
    </div>
    <div v-if="this.numLoaded != 3" class="album-card-container">
      <the-loader></the-loader>
    </div>
  </router-link>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { store } from "@/store";
export default defineComponent({
  name: "album-card",
  props: {
    albumTitle: String,
    albumsId: Number,
  },
  data: function () {
    return {
      store: store,
      numLoaded: 0,
    };
  },
  methods: {
    thumbnailPhotos() {
      return this.$attrs.albumPhotos;
    },
    selectAlbum(value: string) {
      this.store.selectedAlbum = value;
    },
    loaded() {
      this.numLoaded = this.numLoaded + 1;
    },
  },
});
</script>

Important note on using this approach is to use v-show instead of v-if on the div. v-show puts element in html(and sets display:none), while the v-if does not render element in html so images are never loaded.

jckob
  • 41
  • 3