scrollIntoView issue with vue-router

Posted on 25 April 2020 - 1 min read

Updated (2020-04-30):

It turned out that vue-router couldn't handle the hash from the URL due to encoding issue and some weird behaviour in their scrolling logic. A more elegant fix is to override the scrollBehavior function in main.js like:

  router.options.scrollBehavior = (to, _from, savedPosition) => {
    if (savedPosition) {
      return savedPosition;
    }

    if (to.hash) {
      return {
        selector: decodeURI(to.hash),
        offset: { x: 0, y: 80 },
      };
    }

    return { x: 0, y: 0 };
  };

With this fix, we can discard all below fixes.

Original:

While I was adding a sidebar for my blog with GridSome, I realized that headings wasn't clickable. It's supposed to move the screen to the heading. I was wondering whether it's a wrong link, so I checked the source codes. However they were generated properly:

<a
  href="#the-issue"
  class="relative flex items-center py-1 text-sm transition transform hover:translate-x-1 font-bold text-primary"
  >The issue</a
>

Clicking #the-issue should scroll the page to that heading.

scrollIntoView doesn't work properly

After googling, it looks like vue-router doesn't work well with scrollTo and sticky div element (link). And I use sticky for my sidebar. Luckily there is a comment to hack around the issue. A guy suggested to add these lines to use scrollIntoView instead of scrollTo:

router.afterEach((to, from) => {
  document.getElementById("app").scrollIntoView();
});

Thus, I've added these lines to my main.js:

router.afterEach((to) => {
  const id = decodeURI(to.hash).slice(1);
  if (id.length !== 0) {
    const el = document.getElementById(id);
    if (el) {
      el.scrollIntoView();
    }
  }
});

These codes find the element by ID and then scroll the page into it.

The above solution works for most cases except when you open the site via the link with heading anchor for the first time. In order to be able scroll to heading I added:

  mounted() {
    this.$nextTick(() => {
      this.initObserver();
      const id = this.$route.hash.slice(1);
      if (id.length !== 0) {
        const el = document.getElementById(id);
        if (el) {
          el.scrollIntoView();
        }
      }
    });
  },

anchor with Vietnamese headings

You may wonder why that I uses decodeURI to get the decoded heading anchor. By vue-router's implementation, hash param in the url is encoded for URI safe. Therefore, it has to be decoded to allow getElementById find the element properly.

Tagged with: vue, gridsome

Previous: Blogging with VuePress

Next: I patched a npm package

Bong Nguyen © 2016 - 2022