Splide with a rolling window of 21 items
Hello,
It's a project with Laravel and VueJS.
I'm using Splide to display a carousel.
To avoid to have too many requests, I'd like to load a window around the central item displayed on the screen. For example if I display the first item among 33, I want to display item 1 and also load the 10 previous and 10 next items. As it's the first one to display, I will load the items from 24 to 33, item 1 and then items from 2 to 11.
And to avoid to load for example 500 items, the idea is to have in memory only a maximum of 21 items (10 before + 1 displayed + 10 after).
Here is the code of the controller.
public function index(Request $request)
{
$search = $request->search;
$category_id = (int) $request->category_id;
$total = Recipe::
whereHas('media')
->when($search, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search.'%');
})
->when($category_id, function ($query) use ($category_id) {
$query->where('category_id', $category_id);
})
->count();
$window = 21;
$offset = 10;
$index = $request->index ?? 0;
$start = ($index - $offset + $total) % $total;
$end = ($index + $offset) % $total;
if ($start <= $end) {
$recipes = Recipe::
with('category', 'media')
->whereHas('media')
->when($search, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search.'%');
})
->when($category_id, function ($query) use ($category_id) {
$query->where('category_id', $category_id);
})
->orderBy('name')
->skip($start)
->take($window)
->get();
} else {
$previous = Recipe::
with('category', 'media')
->whereHas('media')
->when($search, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search.'%');
})
->when($category_id, function ($query) use ($category_id) {
$query->where('category_id', $category_id);
})
->orderBy('name')
->skip($start)
->take($total - $start)
->get();
$next = Recipe::
with('category', 'media')
->whereHas('media')
->when($search, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search.'%');
})
->when($category_id, function ($query) use ($category_id) {
$query->where('category_id', $category_id);
})
->orderBy('name')
->take($end + 1)
->get();
$recipes = $previous->merge($next);
}
return new RecipeIndexCollection($recipes);
}
And the one of the front.
<template>
<div class="space-y-2">
<div class="text-xl font-bold">
{{ categories.find(category => category.id === mainStore.filters.category_id)?.name ?? 'Recettes' }}
</div>
<template v-if="recipes.length > 0">
<Splide ref="slider" id="splide" :options="splideOptions" @splide:move="loadMore">
<SplideSlide
v-for="recipe in recipes"
:key="recipe.id"
>
<div class="relative h-80" @click="goToRecipe(recipe.id)">
<img v-if="recipe.image_url" class="rounded-xl h-full w-full object-cover" :data-splide-lazy="recipe.image_url" alt="{{ recipe.name }}" />
<div v-if="!recipe.image_url" class="flex justify-center items-center rounded-xl h-full bg-gradient-to-b from-gray-700 to-gray-300 text-gray-700">
<div class="-mt-16">
<font-awesome-icon icon="fa-solid fa-bowl-rice" size="6x" fixed-width></font-awesome-icon>
</div>
</div>
<div class="backdrop-blur-xs absolute top-3 left-3 bg-gray-900/70 rounded-xl px-3 py-1">
<div class="text-[12px] text-gray-50 font-bold">{{ recipe.category.name }}</div>
</div>
<div class="flex flex-col justify-between backdrop-blur-xs absolute h-22 bottom-3 left-3 right-3 bg-gray-900/70 rounded-lg px-3 py-2">
<div class="flex justify-between">
<div class="text-sm text-gray-50 font-bold">{{ recipe.name }}</div>
<div class="text-gray-500">
<font-awesome-icon icon="fa-solid fa-heart" size="xl" fixed-width></font-awesome-icon>
</div>
</div>
<div class="text-sm text-gray-400">{{ recipe.total_time+' min' }}</div>
</div>
</div>
</SplideSlide>
</Splide>
</template>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import recipeService from '@/services/recipeService.js';
import categoryService from '@/services/categoryService.js';
import { Splide, SplideSlide } from '@splidejs/vue-splide';
import { useMainStore } from '@/stores/mainStore.js';
import { watchDebounced } from '@vueuse/core';
const mainStore = useMainStore();
const router = useRouter();
const categories = ref([]);
const recipes = ref([]);
const index = ref(0);
const slider = ref(null);
const splideOptions = ref({
rewind: false,
type: 'slide',
arrows: true,
pagination: false,
lazyLoad: 'nearby',
preloadPages: 5,
perPage: 4,
perMove: 1,
gap: '1em',
start: 10,
breakpoints: {
1024: {
perPage: 3,
},
800: {
perPage: 2,
},
600: {
perPage: 1,
},
},
});
onMounted(async () => {
mainStore.filters.search = null;
mainStore.filters.category_id = null;
const res = await categoryService.index();
if (res.status === 200) {
categories.value = res.data;
}
index.value = 0;
await loadDatas(null);
});
const loadDatas = async (direction) => {
const res = await recipeService.index({ index: index.value, ...mainStore.filters });
if (res.status !== 200) return;
const newItems = res.data.data;
if (direction === 'right') {
recipes.value.push(...newItems);
recipes.value.splice(0, newItems.length);
}
else if (direction === 'left') {
recipes.value.unshift(...newItems);
recipes.value.splice(WINDOW, newItems.length);
}
else {
recipes.value = newItems;
}
}
const loadMore = (event, newIndex, oldIndex, destIndex) => {
if (newIndex > oldIndex) {
if (newIndex >= 21 - 5) {
index.value += 5;
loadDatas('right');
slider.value?.splide.go(newIndex - 5);
}
}
else {
if (newIndex <= 5) {
index.value -= 5;
loadDatas('left');
slider.value?.splide.go(newIndex + 5);
}
}
}
watchDebounced(
mainStore.filters, () => {
loadDatas();
}, { debounce: import.meta.env.VITE_SEARCH_DEBOUNCE, maxWait: 1000 },
);
const goToRecipe = (id) => {
router.push({ name: 'recipes.show', params: { id: id } });
};
</script>
It works quite fine except one thing : as a new set of items is loaded, it generates a little shake while adding and removing some items.
slider.value?.splide.go(newIndex + 5); // or - 5
If I only add new items without removing any (so the items count grows), I don't have any problem. But I'd like to avoid having 500 items loaded. So I want to limit the number of loaded items to 21.
Do you have any idea how to avoid this little shake ?
Thanks for your help.
V
Please or to participate in this conversation.