I need to display Image that I get from external api.
I have ProductCard.vue
where I fetch the image in mounted
hook.
<template>
<q-card
flat
bordered
>
<q-img
:src="PRODUCT_IMG_SRC"
sizes="(max-width: 400px) 400w,
(min-width: 400px) and (max-width: 800px) 800w,
(min-width: 800px) and (max-width: 1200px) 1200w,
(min-width: 1200px) 1600w"
loading="eager"
/>
<q-card-section>
<div >
{{ truncate(product.description, 100) }}
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { truncate } from 'src/utils/textUtils';
import fallback_image from 'src/assets/dluhopis_houses_responsive.svg';
export default defineComponent({
name: 'ProductCard',
props: {
product: Object,
chosen: Boolean,
},
setup(props) {
const PRODUCT_IMG_SRC = ref();
onMounted(async() => {
const IMG_URL = `https://ydc.datanasiti.cz/api/product/${props?.product?.externalId}/image`;
const response = await fetch(IMG_URL);
if (response.status === 200) {
return (PRODUCT_IMG_SRC.value = IMG_URL);
} else {
PRODUCT_IMG_SRC.value = fallback_image;
}
});
return {
truncate,
PRODUCT_IMG_SRC,
};
},
});
</script>
I load this component in parent component in v-for
loop where I'm looping through computed property PageProducts
which filters the products per page.
<template>
<div
v-if="pageProducts"
:
>
<template v-if="!productChosen">
<div
v-for="(product, index) in pageProducts"
:key="`md-${index}`"
>
<product-card
:product="product"
@product-detail="toggleDetail"
@choose-product="chooseProduct"
/>
</div>
</template>
<div
>
<q-pagination
v-if="!productChosen"
v-model="currentPage"
:max="pagesCount()"
direction-links
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, ref, toRefs, computed } from 'vue';
import ProductCard from 'src/components/ProductCard.vue';
import { products } from 'src/api/api';
import { AxiosResponse } from 'axios';
import useDate from 'src/utils/useDate';
import { useQuasar } from 'quasar';
import { IProduct, PAYOUT_FREQUENCY } from 'src/interfaces/Purchase';
export default defineComponent({
name: 'ProductCatalog',
components: {
ProductCard,
},
emits: ['chosen'],
// eslint-disable-next-line no-unused-vars
setup(props, { emit }) {
const $q = useQuasar();
const productsRef = ref([]);
const currentPage = ref(1);
const searchProduct = ref('');
const productChosen = ref();
const PAGE_SIZE = 8;
const { formatDateDDMYYYY, yearsDiff } = useDate();
const dialogState = reactive({
expanded: false,
product: {} as IProduct
});
onMounted(() => {
products.getProducts().then((response: AxiosResponse) => {
productsRef.value = response.data[0];
});
});
const toggleDetail = (product: any) => {
dialogState.product = product;
dialogState.expanded = true;
};
const chooseProduct = (product: IProduct, isChosen: boolean) => {
productChosen.value = product;
if(product) {
$q.notify({
message: 'Produkt byl úspěšně vybrán.',
icon: 'check_circle',
color: 'positive',
});
}
emit('chosen', isChosen, product);
dialogState.expanded = false;
};
const pagesCount = (() => {
if (productsRef.value.length > PAGE_SIZE) {
return Math.ceil(productsRef.value.length / PAGE_SIZE);
}
return 1;
});
const pageProducts = computed(() => {
const pageProducts = productsRef.value;
if (pagesCount() > 1) {
pageProducts = productsRef.value.slice(
(currentPage.value * PAGE_SIZE) - PAGE_SIZE,
currentPage.value * PAGE_SIZE
);
}
if (searchProduct.value) {
return productsRef.value.filter((product: any) => {
return searchProduct.value
.toLowerCase()
.split(' ')
.every((v: any) => product.name.toLowerCase().includes(v));
});
} else {
return pageProducts;
}
});
/* eslint-disable-next-line vue/script-indent */
const payoutFrequency = computed(() => {
const frequency = {
1: PAYOUT_FREQUENCY.MONTHLY,
3: PAYOUT_FREQUENCY.QUATERLY,
6: PAYOUT_FREQUENCY.SEMI_ANNUAL,
12: PAYOUT_FREQUENCY.ANNUAL
} as any;
return frequency[dialogState.product.frequency];
});
return {
productsRef,
pageProducts,
pagesCount,
searchProduct,
currentPage,
payoutFrequency,
productChosen,
chooseProduct,
toggleDetail,
formatDateDDMYYYY,
yearsDiff,
...toRefs(dialogState)
};
},
});
</script>
The problem is that in child component ProductCard.vue
the mounted method is not called for every product when the computed propety in parent component change. It's called only for first 8 products and then when I switch from 7th page to 4th. That's very strange.
Any hint why is this happening? Or do you know any approach how to make this better ?
CodePudding user response:
When Vue mounts a component, it only happens once as part of a component lifecycle. After that Vue's reactivity system simply updates the DOM as reactive data changes. So quite rightly your #mounted
hook is only firing once.
CodePudding user response:
Following up to my previous post, if the products your are passing to your template in the interator (v-for) are actually changing (eg due to pagination) and you need to re-render the component entirely, you need to key the component to inform Vue that your reactive prop has changed at that the component should be re-rendered from scratch.
<product-card
:product="product"
@product-detail="toggleDetail"
@choose-product="chooseProduct"
:key="product.id"
/>