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 invendor.photos.length
photoCount
should becomputed
. Making it separate state runs the risk of not being in sync withvendor.photos.count
, which could lead to subtle bugs. In principle, derived state (e.g:computed
, storegetters
) 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 increated
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 inmounted
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 watchvendor
and setdisplayedPhoto
to0
onvendor
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.