I need to make a composable universal.
Here's my case, I have 3 components - ItemSwiper
, ItemCard
and ViewRecipeDetail
.
ItemSwiper
contains card slides and it is the parent of ItemCard
.
ItemSwiper
has a loop of recipes:
<Slide v-for="recipe in storeRecipe.data" :key="recipe.recipe_id">
<ItemCard :data="recipe"/>
</Slide>
Here I'm passing the data prop to ItemCard
.
Then, in the ItemCard
I use this prop to display the information:
<template>
// here I'll add a skeleton loader that will be shown when the image is loading.
<img class="card__image" :src="getSrc('.jpg')" :alt="data.alt" />
</template>
<script setup>
const props = defineProps(['data', 'pending']);
const isLoaded = ref(false);
const getSrc = ext => {
return new URL(
`../assets/images/content/recipe/${props.data.image}${ext}`,
import.meta.url
).href;
};
onMounted(() => {
const img = new Image(getSrc('.jpg'));
img.onload = () => {
isLoaded.value = true;
};
img.src = getSrc('.jpg');
});
</script>
I need to use this getSrc
function and image preload in the onMounted
hook in another component - ViewRecipeDetail
, which isn't related to these two. In ViewRecipeDetail
will be displayed the detailed information about a recipe.
I was thinking of moving this function and a hook into composable useRecipe
and then use this composable in ItemCard
and in ViewRecipeDetail
.
However, due to the fact that in ItemSwiper
I pass the data
prop, which has recipe
as its value, i.e. recipe in a loop, if I pass this prop as a parameter like this:
import { useRecipe } from '@/composable/useRecipe';
const props = defineProps(['data', 'pending']);
const { isLoaded, getSrc } = useRecipe(props.data);
Then in useRecipe
we can use it like this:
import { onMounted, ref } from 'vue';
export function useRecipe(data) {
const isLoaded = ref(false);
const getSrc = ext => {
return new URL(
`../assets/images/content/recipe/${data.image}${ext}`,
import.meta.url
).href;
};
onMounted(() => {
const img = new Image(getSrc('.jpg'));
img.onload = () => {
isLoaded.value = true;
};
img.src = getSrc('.jpg');
});
return {
isLoaded,
getSrc,
};
}
This will work for ItemCard
, but it won't work for ViewRecipeDetail
. Because I don't need any loop in ViewRecipeDetail
. All I need to do is go to that recipe detail page and see the relevant information for that particular recipe.
It turns out that useRecipe
is not universal now. We pass props.data
as a parameter and it works for ItemCard
, but it won't work for ViewRecipeDetail
, because we need storeRecipe.data
instead of props.data
.
And here's ViewRecipeDatail
. Please, tell me if I'm doing something wrong. I want to display an image same as I did in ItemCard
, using a composable, but without a prop:
<template>
<img
class="card__image"
:src="getSrc('.jpg')"
:alt="storeRecipe.data.alt" />
<div class="card__content">
<h2 class="card__title">
{{ storeRecipe.data.title }}
</h2>
<p class="card__text">
{{ storeRecipe.data.short_description }}
</p>
</div>
</template>
<script setup>
import { useStoreRecipe } from '@/store/storeRecipe';
import { useRecipe } from '@/composable/useRecipe';
const storeRecipe = useStoreRecipe();
const { getSrc } = useRecipe(storeRecipe.data);
</script>
Please, give me a possible solution. (If you don't understand something, please, let me know).
I spent too much time on this :p but I wanted to see if composable was possible for what you want to do
There are a few changes to the composable, it returns a src
and isLoaded
(though, that's a bit redundant as src
is null
and isLoaded
is false
until the image loads, at which point src
has the href, and isLoaded
becomes true - you can see the redunancy there.
Anyway
The composable, useRecipe.js
import { ref, toValue, watchEffect } from "vue";
export function useRecipe(data, ext) {
const isLoaded = ref(false);
const src = ref(null);
const getSrc = () => {
isLoaded.value = false;
const imgvalue = toValue(data)?.image;
if (imgvalue) {
const url = new URL(`../assets/images/content/recipe/${imgvalue}${ext}`, import.meta.url).href;
const img = new Image();
img.onload = () => {
src.value = url;
isLoaded.value = true;
};
img.src = url;
}
};
watchEffect(() => {
getSrc();
});
return {
isLoaded,
src,
};
}
ItemCard.vue
- changed for the alternative I propose
<script setup>
import { useRecipe } from "@/composables/useRecipe";
const props = defineProps(["data"]);
const { src, isLoaded } = useRecipe(props.data, ".jpg");
</script>
<template>
<img class="card__image" :src="src" :alt="data.alt" />
</template>
ItemSwiper.vue
- unchanged
ViewRecipeDetail.vue
- I'm assuming storeRecipe.data
here is NOT an Array, and not the same as storeRecipe.data
in ItemSwiper.vue
, since you don't treat it like an array in your code! Also, your description suggests this is a single recipe. Not sure why the variable name is the same as in ItemSwiper though.
<script setup>
import { useStoreRecipe } from '@/store/storeRecipe';
import { useRecipe } from "@/composables/useRecipe";
const storeRecipe = useStoreRecipe();
const { src, isLoaded } = useRecipe(storeRecipe.data, ".jpg");
</script>
<template>
<img class="card__image" :src="src" :alt="data.alt" />
<div class="card__content">
<h2 class="card__title">
{{ storeRecipe.data.title }}
</h2>
<p class="card__text">
{{ storeRecipe.data.short_description }}
</p>
</div>
</template>