Home > database >  Rotate through img's that are created with fetched data
Rotate through img's that are created with fetched data

Time:10-16

I have a working solution for this, but it seems pretty messy to me and not the correct way to do this in Vue.

I need to fetch data from a backend about a "Vendor". The vendor has photos that should be displayed on the page. I want one photo shown at a time, then rotate through them every 5s by changing their opacity, using setInterval.

I have a 'ref' for the img's. However, I cannot use it in '.then' because this.$refs is not available in created(). In this case, the "refs" are not available in mounted() either due to the asynchronous fetch in created().

I obviously cannot put setInterval in update, because it creates a new listener for every update (yes, I'm an idiot and actually tried that...).

Right now, I have updated() setting this.photoCount every time it updates. setInterval is added in created() and does nothing until this.photoCount is no longer null.

<template>
    <notification-banner
        v-show="banner.displayed"
        :type="banner.type"
        :message="banner.message"
    ></notification-banner>

    <div >
        <svg  width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" color="#000000">
            <path d="M12 15a3 3 0 100-6 3 3 0 000 6z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
            <path d="M19.622 10.395l-1.097-2.65L20 6l-2-2-1.735 1.483-2.707-1.113L12.935 2h-1.954l-.632 2.401-2.645 1.115L6 4 4 6l1.453 1.789-1.08 2.657L2 11v2l2.401.655L5.516 16.3 4 18l2 2 1.791-1.46 2.606 1.072L11 22h2l.604-2.387 2.651-1.098C16.697 18.831 18 20 18 20l2-2-1.484-1.75 1.098-2.652 2.386-.62V11l-2.378-.605z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
        </svg>

        <div >
            <h1>{{vendor.name}}</h1>

            <p>{{vendor.email}}</p>
            
            <div >
                <p>market.com/{{vendor.url}}</p>

                <svg @click="copyUrl" width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" color="#000000">
                    <path d="M19.4 20H9.6a.6.6 0 01-.6-.6V9.6a.6.6 0 01.6-.6h9.8a.6.6 0 01.6.6v9.8a.6.6 0 01-.6.6z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
                    <path d="M15 9V4.6a.6.6 0 00-.6-.6H4.6a.6.6 0 00-.6.6v9.8a.6.6 0 00.6.6H9" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
                </svg>
            </div>
        </div>

        <div >
            <h1>{{vendor.name}}</h1>

            <div >
                <div  v-for="(photo, i) in vendor.photos" :key="i">
                    <img ref="vendorPhoto" :src="`http://localhost:8000${photo}`" alt="Vendor provided image" :/>
                </div>
            </div>

            <p>{{vendor.description}}</p>
        </div>
    </div>
</template>

<script>
export default {
    data(){
        return {
            vendor: {},
            banner: {
                displayed: false,
                type: "",
                message: ""
            },
            displayedPhoto: 0,
            photoCount: null
        }
    },

    created(){
        let token = localStorage.getItem("jwt");
        let headers = {"Content-Type": "application/json"};
        if(token !== null) headers["Authorization"] = `Bearer ${token}`;

        fetch(`http://localhost:8000/vendor${document.location.pathname}`, {
            method: "get",
            headers: headers,
        })
            .then(r=>r.json())
            .then((vendor)=>{
                this.vendor = vendor;
            })
            .catch((err)=>{
                console.error(err);
            });

        setInterval(()=>{
            if(this.photoCount !== null){
                if(this.displayedPhoto >= this.photoCount){
                    this.displayedPhoto = 0;
                }else{
                    this.displayedPhoto  ;
                }
            }
        }, 5000);
    },

    updated(){
        this.photoCount = this.$refs.vendorPhoto.length;
    }

How could I do this in a better, more "vue-like" way? My solution, while working, seems like garbage.

CodePudding user response:

What I'd change:

  • There's no need to count $refs. The information is already available in vendor.photos.length
  • photoCount should be computed. Making it separate state runs the risk of not being in sync with vendor.photos.count, which could lead to subtle bugs. In principle, derived state (e.g: computed, store getters) should always be derived state, rather than keeping the same information in two separate places in state.
  • the displayedPhoto increment function could be simplified to:
methods: {
  changePhoto() {
    this.displayedPhoto = (this.displayedPhoto   1) % this.photoCount
  }
}
  • fetching vendor should be a standalone method (e.g: fetchVendor). This allows re-fetching whenever the business logic requires it, it's no longer coupled to component lifecycle
  • I moved fetching in mounted. created is supposed to only hold code that needs to run before the component was added to DOM. Which is not the case for fetching data.
    Fetching in created creates the false impression the component might get away with not having to deal with rendering before fetch returned. Which is never true, not even when the backend runs on the same machine.
    I'd rather fetch in mounted and handle "loading..." state adequately (e.g: a loading indicator, a funny picture, etc...)
  • I'd like to draw attention to:
    { hidden: i !== displayedPhoto % photoCount }. I've added the % photoCount part for an edge case: if/when you switch from a vendor having, say 10 pictures, to one with, say 5 pictures, if the displayed photo index is above 5, no picture will be visible until the interval fn runs again. Adding % photoCount makes sure a picture of the new vendor is displayed. Alternatively, we could watch vendor and set displayedPhoto to 0 on vendor changed.
  • speaking of swapping vendor, I also added a more robust way of handling the "slider", making sure no interval is left running, in any scenario.

See it here. Notes:

  • I had to mock the axios request with a promise returning an approximation of the actual call's response.
  • I made a custom fader for the images, since you haven't shared that code

That's about it.

  • Related