javascriptvue.jsvuejs3vue-componentintersection-observer

vue 3 composition api, can not return value from a helper function to component


i think this is a problem of scope inside my helper function, i have successfully implementing the same logic in my component and it works perfectly

i have already using a composable function to use ref() with the variable that i want to return

=> this is the working code inside my component

// component.vue
setup() {
    const subnavDiv = ref(false) // this is the variable that the bellow code update

    onMounted(() => {
      const routEl = document.querySelector('.main')
      const fixedNav = document.querySelector('.main-nav')
      const el = document.querySelector('.search')

      let prevYPosition = 0
      let directionVar = 'up'
      const options = {
        root: routEl,
        rootMargin: `${fixedNav.offsetHeight * -1}px`,
        threshold: 0.7
      }

      const setScrollDirection = () => {
        if (routEl.scrollTop > prevYPosition) {
          directionVar = 'down'
        } else {
          directionVar = 'up'
        }
        prevYPosition = routEl.scrollTop
      }

      const onIntersection = ([e]) => {
        setScrollDirection()
        if ((directionVar === 'down' && e.boundingClientRect.top > 0) || !e.isIntersecting) {
          console.log('down')
          subnavDiv.value = true
        } else {
          subnavDiv.value = false
        }
        if (
          e.isIntersecting &&
          (e.boundingClientRect.top < fixedNav.offsetHeight ||
            e.boundingClientRect.top > fixedNav.offsetHeight)
        ) {
          subnavDiv.value = false
        }
      }

      const observer = new IntersectionObserver(onIntersection, options)
      observer.observe(el)
    })

    return {
      subnavDiv
    }
  }
}
}

=> after moving the same code out of the component

// component.vue
setup() {
    const subnavDiv = ref(false)

    onMounted(() => {
      const rootEl = document.querySelector('.main')
      const fixedNav = document.querySelector('.main-nav')
      const el = document.querySelector('.search')

      subnavDiv.value = onIntersect(rootEl, 0.7, fixedNav, el) // the helper function call: this line is supposed to update the value of subnavDiv
    })
})


///////////////////////  $$$$$$$$$$$$$$$$$$  ///////////////

onIntersect.js // the helper function
const onIntersect = (rootElement, thresholdValue, elementToChange, elementToWatch) => {
  let prevYPosition = 0
  let directionVar = 'up'
  const options = {
    root: rootElement,
    rootMargin: `${elementToChange.offsetHeight * -1}px`,
    threshold: thresholdValue
  }
  let bool = false // this is the variable that i am trying to return from this function

  const setScrollDirection = () => {
    if (rootElement.scrollTop > prevYPosition) {
      directionVar = 'down'
    } else {
      directionVar = 'up'
    }
    prevYPosition = rootElement.scrollTop
  }

  const onIntersection = ([e]) => {
    setScrollDirection()
    if ((directionVar === 'down' && e.boundingClientRect.top > 0) || !e.isIntersecting) {
      console.log('down')
      bool = true
    } else {
      bool = false
    }
    if (
      e.isIntersecting &&
      (e.boundingClientRect.top < elementToChange.offsetHeight ||
        e.boundingClientRect.top > elementToChange.offsetHeight)
    ) {
      bool = false
    }
  }

  const observer = new IntersectionObserver(onIntersection, options)
  observer.observe(elementToWatch)

  return bool // the return (not returning any thing)
}

export default onIntersect

Solution

  • The important part is the ref, the purpose of the ref pattern is to pass a value by reference between scopes. onIntersect is supposed to be a composable and follow regular conventions for them. It's supposed to be called in setup rather than lifecycle hooks.

    Direct DOM access is not desirable in Vue. If main, etc are created in the same components, they should be template refs, otherwise they can be provided to the composable as refs, so it could make use of reactivity. The use of onMounted inside a composable would make it inflexible and prone to race conditions. Considering that elementToWatch needs to be accessed first, it could be:

    const useIntersect = (rootElRef, threshold, changedElRef, watchedElRef) => {
      const bool = ref(false);
      let observer;
    
      watchEffect(() => {
        const rootEl = unref(rootElRef);
        const changedEl = unref(changedElRef)
        const watchedEl = unref(watchedElRef);
    
        if (rootEl && watchedEl) {
          ...
          observer = new IntersectionObserver(onIntersection, options)
          observer.observe(watchedEl)
        }
    
        return () => {
          // clean up
          observer?.disconnect();
        }
      });
    
      return bool;
    };
    

    DOM elements can still be accessed directly if necessary, but they are refs any way:

    ...
    const rootElRef = ref(null);
    const isSubnav = useIntersect(rootElRef, ...);
    onMounted(() => {
      rootElRef.value = document.querySelector('.main');
      ...
    

    As a side effect, it can react to the changes in parameters, e.g. it can be forced to recreate an observer if the options need to be changed.