Search code examples
javascriptvue.jsvuejs2vue-componentintersection-observer

Vue set up IntersectionObserver


I am trying to set up an IntersectionObserver for an infinite scroll list. I want to check with the IntersectionObserver if the last item of a list has been reached.

So far my IntersectionObserver setup looks like this:

mounted() {
    const config = {
        treshold: 1
    };

    const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
            if (entry.intersectionRatio > 0) console.log(entry.target);
        });
    }, config);

    observer.observe(this.lastRenderedDeal());
}

My list items are rendered like this:

<deal :deal="deal"
      :key="index"
      ref="deals"
      v-for="(deal, index) in deals"
</deal>

To fetch the last renderedDeal I use the ref on the deals:

lastRenderedDeal() {
    const deals = this.$refs.deals;
    return deals ? deals[deals.length - 1].$el : null;
}

This works for the initial setup and it triggers. The problem is that when I want infinite scroll I need to keep appending to the list of deals. So my lastRenderedDeal constantly updates to reflect the last deal that was appended to my list.

This reactivity is not being passed on to the observer.observe method it seems. It only picks up my initial element. This seems kind of obvious since it has been instantiated in the mounted hook but how can I deal with this?

Do I need to set a watcher for the deals and call the observer.observe again? If so, can I simply tell it to replace the initial observed item?


Solution

  • You can observe a dummy <div> which is always at the bottom of the list. You might need to set a key on the <div> to ensure that Vue won't recreate the element during each render of the component.

    I always like to make a general and reusable component which bundles this behavior. Here's my take:

    const InfiniteScroll = {
      render(h) {
        return h('div', [
          ...this.$slots.default,
    
          // A dummy div at the bottom of the list which we will observe.
          // We must set a key on this element so that Vue reuses
          // the same element it initially created upon each rerender.
          h('div', {
            key: 'footer',
            ref: 'footer',
            style: {
              height: '1px'
            },
          }),
        ]);
      },
    
      mounted() {
        this.observer = new IntersectionObserver(entries => {
          // We only have one entry. Is it visible?
          if (entries[0].intersectionRatio > 0) {
            this.$emit('trigger');
          }
        });
    
        // Observe the dummy footer element
        this.observer.observe(this.$refs.footer);
      },
    };
    
    new Vue({
      el: '#app',
      components: {
        InfiniteScroll,
      },
      data: {
        items: [],
      },
      created() {
        this.loadMore();
      },
      methods: {
        loadMore() {
          for (let i = 0; i < 20; i++) {
            this.items.push(this.items.length + 1);
          }
        },
      },
    });
    body {
      margin: 0;
    }
    
    .item {
      border-bottom: 1px solid #eee;
      padding: 10px;
    }
    <script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
    
    <div id="app">
      <infinite-scroll @trigger="loadMore">
        <div v-for="item of items" class="item">{{ item }}</div>
      </infinite-scroll>
    </div>