Search code examples
javascriptvue.jsvuexnuxt.js

Nuxtjs ScrollSpy through Store


Codesandbox: https://codesandbox.io/s/romantic-pond-ehixi?file=/pages/index.vue

Hi, let me explain the design first before the problem so we all are on the same view 🙂

I am currently new to Vue and working on a Nuxtjs project with the Vuex store. In my layout/default.vue, I have a Header, Footer component, and <Nuxt /> between them. Both Header & Footer will be fixed/sticky in the top and bottom. The main content will be scrollable and it has cards inside it.

⚙️ Problem:

The Header component will have navigation which will get highlighted when the correct card is in the viewport (at least 100px offset). In the Footer component, there are buttons to navigate the user to previous or next card (100px offset included) depending on what card the user is looking at.

I am using vue2-scrollspy but it's not working (see codesandbox). I am also not sure if this is the right module to do the job.

⚠️ UPDATE 01

Scroll Spy is working now when the user scrolls the page & I added a watcher to update the store values. The remaining issue is to scroll the user to the previous or next card depending on what they are viewing. On vue2-scrollspy document, it says the following.

$scrollTo(index: int) is provided on scope Vue instance to invoke a scroll to the given section index.

However, calling $scrollTo(0) or this.$scrollTo(0) giving me an error:

this.$scrollTo is not a function

Codesandbox has been updated with the latest changes 🙏


Solution

  • Ok so this is how I fix my issue 🙏

    On the /pages/index.vue I added ID on every card and a watcher to check every time scroll spy change count value. If count change, I update the value on the store.global [currentView, previousView, nextView].

    📄 /pages/index.vue

    <template>
      <main class="main" v-scroll-spy="{ data: 'count', offset: 250 }">
        <div
          :key="item"
          v-for="(item, index) in sections"
          :id="`view-${index}`"
          class="main-child card"
          :class="[`bg-${index}`]"
        >
          <h1>{{ item }}</h1>
          <p>{{ `${index} ${item} ${count}` }}</p>
        </div>
      </main>
    </template>
    
    <script>
    export default {
      data() {
        return {
          count: 0
        };
      },
      computed: {
        sections() {
          return this.$store.state.sections;
        }
      },
      watch: {
        count(newValue) {
          this.$store.dispatch("updateCurrentView", newValue);
          this.$store.dispatch(
            "updatePreviousView",
            newValue === 0 ? 0 : newValue - 1
          );
          this.$store.dispatch(
            "updateNextView",
            newValue === this.sections.length - 1
              ? this.sections.length - 1
              : newValue + 1
          );
        }
      }
    };
    </script>
    

    🌍 /store/index.js

    export const state = () => ({
      global: {
        currentView: 0,
        previousView: 0,
        nextView: 1
      },
      sections: ["first", "second", "third", "fourth", "fifth", "sixth"]
    });
    
    export const mutations = {
      UPDATE_CURRENT_VIEW(state, value) {
        state.global.currentView = value;
      },
      UPDATE_PREVIOUS_VIEW(state, value) {
        state.global.previousView = value;
      },
      UPDATE_NEXT_VIEW(state, value) {
        state.global.nextView = value;
      }
    };
    
    export const actions = {
      updateCurrentView({ commit }, value) {
        commit("UPDATE_CURRENT_VIEW", value);
      },
      updatePreviousView({ commit }, value) {
        commit("UPDATE_PREVIOUS_VIEW", value);
      },
      updateNextView({ commit }, value) {
        commit("UPDATE_NEXT_VIEW", value);
      }
    };
    

    On the component where the button up & down is located, I can then call the global state and add actions to scroll to previous or next element base on the global values:

    📦 /components/WithUpDownButtons.vue

    export default {
      computed: {
        global() {
          return this.$store.state.global
        }
        ...
      },
      methods: {
        scrollTopPrevious() {
          // scroll to previous element into view
          // this.$scrollTo(this.previousView) < not working
          document
            .getElementById(`view-${this.global.previousView}`)
            .scrollIntoView(true);
        },
        scrollTopNext() {
          // scroll to next element into view
          // this.$scrollTo(this.nextView) < not working
          document
            .getElementById(`view-${this.global.nextView}`)
            .scrollIntoView(true);
        }
      }
    };
    

    If we need to add scroll animation, we can update the scrollIntoView(true) with the following:

    📦 /components/WithUpDownButtons.vue

    ...
      methods: {
        scrollTopPrevious() {
          // scroll to previous element into view
          document
            .getElementById(`view-${this.global.previousView}`)
            .scrollIntoView({
              top: 0,
              behavior: 'smooth'
            });
        },
        scrollTopNext() {
          // scroll to next element into view
          document
            .getElementById(`view-${this.global.nextView}`)
            .scrollIntoView({
              top: 0,
              behavior: 'smooth'
            });
        }
      }
    ...