javascriptvuejs3inertiajsintersection-observervueuse

vueUse useIntersectionObserver and rootMargin, triggering well before element is within range


I'm really confused what I'm doing wrong here. I'm trying to implement an infinitescroll that will load new items when the bottom edge of the target element is within 200px of the bottom of the viewport but it triggers almost as soon as I start scrolling

/**
 * Custom composable function for implementing infinite scroll functionality.
 *
 * @param {string} propName - The name of the prop that contains the data to be paginated.
 * @param {HTMLElement|null} landmark - The landmark element to observe for intersection. If null, infinite scroll will not be triggered by intersection.
 * @returns {object} - An object containing the paginated items, methods to load more items and reset items, and a computed property indicating if more items can be loaded.
 */

import { computed, nextTick, ref, watch, watchEffect } from 'vue'
import { router, usePage } from '@inertiajs/vue3'
import { onBeforeUnmount } from 'vue';

export function useInfiniteScroll(propName, landmark = null) {

///

    const observer = useIntersectionObserver(landmark, ([{isIntersecting}], observerElement) => {

        const rect = landmark.value.getBoundingClientRect();
        const viewportHeight = window.innerHeight || document.documentElement.clientHeight;

        if (isIntersecting && canLoadMoreItems.value){

            console.log(rect.top, rect.bottom, rect.width, rect.height, viewportHeight, isIntersecting)
            console.log('Root:', observer.root);

            loadMoreItems();
        }
    },
    {

        rootMargin: '0px 0px 200px 0px',
    });

///

}

when it first triggers the log shows:

123 6411 1871 6288 553 true
Root: undefined

Here is the component I am trying to use this composable from:

<script setup>

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head} from '@inertiajs/vue3';
import CreatePost from './Partials/CreatePost.vue';
import ShowPost from './Partials/ShowPost.vue';
import { ref, computed  } from 'vue';
import FollowHeader from '@/Pages/Feed/Partials/FollowHeader.vue';
import { useInfiniteScroll } from '@/composables/useInfiniteScroll';

const props = defineProps({
    posts: {
        type: Object,
    },
    users: {
        type: Object,
    },
    featured : {
        type : Object,
        default: null
    },
    follow : {
        type : Object,
        default: null
    },
    user : {
        type: Object,
        default: null
    },
    tag : {
        type: Object,
        default: null
    }

});

//target for infinite scroll
const target = ref(null);
let { items, canLoadMoreItems } = useInfiniteScroll('posts', target)


//is this a featured post? Then open modal
const featuredPost = ref(props.featured);

const showFeatured = computed(() => {
    return featuredPost.value != null;
})

const isFeatured = computed(() => {
    return featuredPost.value != null;
})

//inject trans to pull in title string
const trans = inject('trans')
const title = computed(() => {
    if (props.user){
        return '@' + props.user.username;
    }
    else if (props.tag){
        return '#' + props.tag.tagname
    }

    return trans('feed.feedTitle');
})


</script>

<template>

    <Head :title="title" />

    <AuthenticatedLayout :class="{'blur-sm' : showFeatured}">

        <template #header>
            <div class="flex items-center space-x-3" dusk="feed-header">

                <FollowHeader v-if="user || tag" :user="user" :tag="tag" :follow="follow"/>
                <h2 v-else class="font-semibold text-xl text-gray-800 leading-tight">{{title}}</h2>

            </div>

        </template>


        <div v-show="!user && !tag" class="sm:py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white  shadow sm:rounded-lg">
                    <div class="p-2 sm:p-6 text-gray-900">
                        <CreatePost :placeholder="__('feed.postPrompt', {'username': $page.props.auth.user.username})" />
                    </div>
                </div>
            </div>
        </div>

        <div class="py-2 sm:py-6" ref="target" >
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 sm:space-y-3 space-y-2" >
                <div v-for="post in items" class="bg-white sm:rounded-lg sm:p-3 my-2" dusk="post">
                    <ShowPost :users="users" :post="post" :descendants="post.descendants" :currentDepth="1" @show-post="toggleFeatured(post.id)"/>
                </div>
            </div>
        </div>

    </AuthenticatedLayout>
</template>

Have I somehow got the wrong root set?


Solution

  • It's not fully an answer, but I used the new scrollMargin instead rootMargin which worked.

    However, I had other issues as I was trying to create an infinite scroll that would accept scrolling up as well as down. e.g. a private message chat where we would usually want to scroll up and have new items added a the top of the feed, not the bottom.

    For anyone else who gets stuck with this, this is a modified version that's now working for me:

    usage: let { items, canLoadMoreItems, loading } = useInfiniteScroll('posts', infiniteScrollElement,'down', 500)

    /**
     * Custom composable function for implementing infinite scroll functionality.
     *
     * @param {string} propName - The name of the prop that contains the data to be paginated.
     * @param {HTMLElement|null} landmark - The element to observe for intersection. If null, infinite scroll will not be triggered by intersection. *** N.B. Must be a scrollable element with a fixed height
     * @param {string|down} scrollDirection - Which direction is user scrolling up/down
     * @param {number|200} triggerDistance - how many pixels from the edge should it trigger.
     * @returns {object} - An object containing the paginated items, methods to load more items and reset items, and a computed property indicating if more items can be loaded.
     */
    
    import { computed, nextTick, ref, watch, watchEffect } from 'vue'
    import { router, usePage } from '@inertiajs/vue3'
    
    
    export function useInfiniteScroll(propName, landmark = null, scrollDirection = 'down', triggerDistance = 200) {
    
        // Store loading state to stop multiple triggers
        const loading = ref(false);
    
        // Get the value of the prop that contains the data to be paginated.
        const value = () => usePage().props[propName]
    
        // Create a ref to store all the  paginated items.
        const items = ref(value().data)
    
        // Watch for changes to prop and update items accordingly.
        // Allows itemsto be added from an external source
        watch(() => value().data, (newValue) => {
            if (!loading.value) {
                items.value = newValue;
            }
        }, { deep: true });
    
        // Get the initial URL of the page so we can replace the URL after loading more items.
        const initialUrl = usePage().url
    
        // Computed property to check if more items can be loaded.
        const canLoadMoreItems = computed(() => value().next_page_url !== null)
    
        // Create the listeners for scoll on the landmark element
        if (landmark != null) {
            nextTick(() => {
                landmark.value.addEventListener('scroll', () => {
                        checkPosition();
                });
                landmark.value.addEventListener('touchmove', () => {
                        checkPosition();
                });
            });
        }
    
    
        // Method to check if the landmark element is in range
        const checkPosition = () => {
    
            if (loading.value || !canLoadMoreItems.value) {
                return;
            }
    
            //downward scroll
            if (scrollDirection.toLowerCase() === 'down') {
    
                const elementScrollBottom = landmark.value.scrollHeight - landmark.value.scrollTop;
    
                if (elementScrollBottom - window.innerHeight <= triggerDistance) {
                    loadMoreItems();
                }
            }
    
            //upward scroll
            else if (landmark.value.scrollTop <= triggerDistance) {
                loadMoreItems();
            }
    
    
        };
    
        // Method to load more items.
        const loadMoreItems = () => {
    
            // Set the loading state to true.
            loading.value = true;
    
            // Load more items using Inertia's get method and append the new items to the existing items.
            router.get(
                value().next_page_url,
                {},
                {
                    preserveState: true,
                    preserveScroll: true, //only works for downward scroll
                    onSuccess: () => {
    
                        //reset url to initial url
                        window.history.replaceState({}, '', initialUrl)
    
                        // Capture scroll position
                        const originalScrollPosition = landmark.value.scrollHeight - landmark.value.scrollTop;
    
                        //add new items to the page
                        items.value = [...items.value, ...value().data]
    
                        nextTick(() => {
    
                            // only if its upward scroll reset the scroll position
                            if (scrollDirection.toLowerCase() === 'up') {
                                landmark.value.scrollTop = landmark.value.scrollHeight - originalScrollPosition;
                            }
    
                        });
    
                        loading.value = false;
                    },
                    onError: (error) => {
                        console.error(error);
                        loading.value = false;
                    }
                }
            )
        }
    
    
      // Return the items, loadMoreItems method, resetItems method, and canLoadMoreItems computed property.
      return {
        items,
        loadMoreItems,
        resetItems: () => (items.value = value().data),
        canLoadMoreItems,
        loading
      }
    }