VueUse useIntersectionObserver и rootMargin, срабатывающий задолго до того, как элемент окажется в пределах диапазона

Я действительно смущен, что я делаю здесь неправильно. Я пытаюсь реализовать бесконечную прокрутку, которая будет загружать новые элементы, когда нижний край целевого элемента находится в пределах 200 пикселей от нижней части области просмотра, но она срабатывает почти сразу, как только я начинаю прокрутку.

/**
 * 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.info(rect.top, rect.bottom, rect.width, rect.height, viewportHeight, isIntersecting)
            console.info('Root:', observer.root);

            loadMoreItems();
        }
    },
    {

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

///

}

когда он впервые срабатывает, журнал показывает:

123 6411 1871 6288 553 true
Root: undefined

Вот компонент, из которого я пытаюсь использовать эту составную часть:

<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>

Я как-то неправильно установил root?

Можно ли поделиться кодом вашего шаблона vue? Есть ли у вас атрибут v-intersection-observer или ref = "landmark" в целевом элементе?

nom_unique 07.04.2024 17:25

@nom_unique спасибо за ответ. Я добавил компонент сейчас. это элемент ref = "target" внизу.

Ben 08.04.2024 08:28

Когда вы объявляете свой const Observer = useIntersectionObserver(landmark, ....), откуда берется переменная «ориентир»? Документ useIntersectionObserver, похоже, указывает на то, что вы должны передать ссылку на целевой HTML (в вашем случае «цель») в качестве первого параметра. vueuse.org/core/useIntersectionObserver

nom_unique 09.04.2024 20:57

@nom_unique, извините, я не опубликовал полную useInfiniteScroll композицию, я добавил еще немного, что, надеюсь, имеет больше смысла.

Ben 10.04.2024 08:27

Спасибо! Итак, когда вы передаете здесь «target»: useInfiniteScroll('posts', target), вы передаете полный изменяемый объект ref «target», который не является фактическим элементом HTML. Вам нужно получить доступ к значению ref() с помощью target.value. Попробуйте useInfiniteScroll('posts', target.value) и дайте мне знать, как это работает.

nom_unique 10.04.2024 17:02

Кроме того, если вы попытаетесь получить доступ к значению ref() в setup(), значение будет равно Null, поскольку дерево DOM компонента еще не создано. Вам нужно будет назначить { items, canLoadMoreItems} в хуке onMounted.

nom_unique 10.04.2024 17:06

Еще раз спасибо, что потратили на это время, я это очень ценю. Если я неправильно прочитал документацию, я почти уверен, что useInfiniteScroll ожидает номер ссылки? И я ценю то, что вы говорите о том, что до того, как страница будет отображена, оно будет равно нулю, но я думал, что useInfiniteScroll создает экземпляр прослушивателя, который будет проверять пересечение, поэтому, хотя оно изначально равно нулю, оно не будет на протяжении всего жизненного цикла. vueuse.org/core/useInfiniteScroll/#useinfinitescroll

Ben 12.04.2024 09:31

Вы правы насчет документации! Но, возможно, мне чего-то не хватает... в коде, которым вы поделились, вы создаете свой собственный компонуемый useInfiniteScroll и, похоже, на самом деле не используете useInfiniteScroll из библиотеки VueUse. В вашем компоненте вы можете просто импортировать { useInfiniteScroll } из @vueuse/core, затем вызвать useInfiniteScroll с вашей «целью» в качестве первого параметра и функцией обратного вызова в качестве второго параметра. В обратном вызове вы можете поместить логику, которая у вас есть в компонуемом объекте.

nom_unique 15.04.2024 20:30

Хотя реализация vueuse хороша, я не могу найти способ заставить ее работать при прокрутке элемента вверх. например окно чата обычно содержит самое последнее сообщение внизу, и пользователь прокручивает вверх, чтобы загрузить больше

Ben 16.04.2024 18:26
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
9
126
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это не полный ответ, но вместо этого я использовал новый scrollMarginrootMargin, который сработал.

Однако у меня были и другие проблемы, поскольку я пытался создать бесконечную прокрутку, которая допускала бы прокрутку как вверх, так и вниз. например чат личных сообщений, в котором мы обычно хотим прокрутить вверх и добавить новые элементы вверху ленты, а не внизу.

Для тех, кто застрял в этом, это модифицированная версия, которая теперь работает у меня:

использование: 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
  }
}

Другие вопросы по теме