9
votes

I am trying to make a scrolling to anchor by means of scrollBehaviour in VueJS.

Generally, I change current router with the following way :

this.$router.push({path : 'componentName', name: 'componentName', hash: "#" + this.jumpToSearchField})

My VueRouter is defined as :

const router = new VueRouter({
  routes: routes,
  base: '/base/',
  mode: 'history',
  scrollBehavior: function(to, from, savedPosition) {
    let position = {}
    if (to.hash) {
      position = {
        selector : to.hash
      };
    } else {
      position = {x : 0 , y : 0}
    }
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(position)
      }, 10)
    })
  }
});

My routes :

[
  {
    path: '/settings/:settingsId',
    component: Settings,
    children: [
      {
        path: '',
        name: 'general',
        components: {
          default: General,
          summary: Summary
        }
      },
      {
        path: 'tab1',
        name: 'tab1',
        components: {
          default: tab1,
          summary: Summary
        }
      },
      {
        path: 'tab2',
        name: 'tab2',
        components: {
          default: tab2,
          summary: Summary
        }
      },
      {
        path: 'tab3',
        name: 'tab3',
        components: {
          default: tab3,
          summary: Summary
        }
      }
    ]
  },
  {
    path: '/*',
    component: Invalid
  }
];

Let's say I am on tab1 component and I would like to jump to anchor 'test' on tab3 component

After router.push() I see that scrollBehavior is trigged and component switches from tab1 to tab3 as well as URL is changed (e.g. http://localhost:8080/tab1 to http://localhost:8080/tab3#test) but windows location is not placed where anchor is but just on the top of the window.

And of course, I have textarea with id="test" on tab3 component

What can be wrong ?

11
My initial thought is that when the route is resolved the DOM is not available to scroll to. Have you tried writing a small function to scrollto the element in the url hash on mounted - jonnycraze
I had this issue as well, ended up writing my own functions for it to jump, since I loaded content async so the jump needed to accure after it had loaded the async data, VueJS tries to jump before its loaded. - Anuga
@Anuga did you write your own func in scrollBehaviour ? - Yuriy Korovko
Nah, I combined jQuery scrollTo and some own code, in the component that needed the scrolling. After the "mount" it checks the path and hash and jumps when the page is done loaded. Not a bright solution, but it works flawlessly. - Anuga

11 Answers

8
votes

Use {left: 0, top: 0} instead of {x: 0, y: 0} and it will work.

I think it's a mistake in Vue documentation because if you log savedPosition on the console, you'll see {left: 0, top: 0} and when you change {x: 0, y: 0} like that everything will work perfectly.

8
votes

I couldn't get any of the other solutions around this working, and it was really frustrating.

What ended up working for me was the below:

const router = new Router({
    mode: 'history',
    routes: [...],
    scrollBehavior() {
        document.getElementById('app').scrollIntoView();
    }
})

I mount my VueJs app to #app so i can be certain it is present its available for selection.

2
votes

Alright so I'm a bit late to the party but recently stumbled upon a fairly similar problem. I couldn't make my scrollBehavior work with the anchor. I finally found the root cause: my <router-view> was wrapped in a <transition>, which delayed the render/mounting of the anchor, like so:

<Transition name="fade-transition" mode="out-in">
  <RouterView />
</Transition>

What happened was:

  • You click on your redirect link with anchor
  • Router gets the info and changes the URL
  • <router-view> transition start. New content NOT YET mounted
  • scrollBehavior happens at the same time. The anchor is not found, so no scrolling
  • Transition is over, <router-view> correctly mounted/rendered

Without transition, the scrollBehavior return {selector: to.hash} works fine, since the content is instantly mounted, and the anchor exists in the page.

Because I did not want to remove the transition, I crafted a workaround which periodically tries to get the anchor element, and scrolls to it once it's rendered/found. It looks like this:

function wait(duration) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

async function tryScrollToAnchor(hash, timeout = 1000, delay = 100) {
  while (timeout > 0) {
    const el = document.querySelector(hash);
    if (el) {
      el.scrollIntoView({ behavior: "smooth" });
      break;
    }
    await wait(delay);
    timeout = timeout - delay;
  }
}

scrollBehavior(to, from, savedPosition) {
  if (to.hash) {
    // Required because our <RouterView> is wrapped in a <Transition>
    // So elements are mounted after a delay
    tryScrollToAnchor(to.hash, 1000, 100);
  } else if (savedPosition) {
    return savedPosition;
  } else {
    return { x: 0, y: 0 };
  }
}

1
votes

I'm sharing my 2 cents on this problem for anyone like me looking for a working solution. Picking up on Sweet Chilly Philly, answer which was the only thing that worked for me, I'm adding the relevant code to make the URL hash work aswell:

  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) {
      Vue.nextTick(() => {
        document.getElementById(to.hash.substring(1)).scrollIntoView();
      })
      //Does not work but it's the vue way
      return {selector: to.hash}
    }

    if (savedPosition) {
      //Did not test this but maybe it also does not work
      return savedPosition
    }

    document.getElementById('app').scrollIntoView();
    //Does not work but it's the vue way
    return {x: 0, y: 0}
  }

I won't get into much detail about Vue.nextTick (you can read more about it here) but it kinda runs the code after the next DOM update, when the route already changed and the element referenced by the hash is already ready and can be reached through document.getElementById().

1
votes

this works for me in Vue 3 :

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  scrollBehavior(to, from, SavedPosition) {
    if (to.hash) {
      const el = window.location.href.split("#")[1];
      if (el.length) {
        document.getElementById(el).scrollIntoView({ behavior: "smooth" });
      }
    } else if (SavedPosition) {
      return SavedPosition;
    } else {
      document.getElementById("app").scrollIntoView({ behavior: "smooth" });
    }
  },
});
0
votes

Check out vue-routers support for this feature:

https://router.vuejs.org/guide/advanced/scroll-behavior.html

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash
      // , offset: { x: 0, y: 10 }
    }
  }
}
0
votes

This can work too if jQuery is available:

scrollBehavior (to, from, savedPosition) {    
  $('#selector-in-element-with-scroll').scrollTop(0)
}
0
votes

I had a similar problem which was caused by following some example I found online. The problem in my case was that the item was not yet rendered. I was going off the after-leave event of a transition and though it threw no errors, it wasn't scrolling to the element. I changed it to the enter event of the transition and it works now.

I know the question didn't mention transitions, so maybe in this case you could try nextTick rather than setTimeout to make sure the element has rendered.

0
votes

None of the above suggestions worked for me:

What I found and it works perfectly for my case is this:

App.vue

 <transition @before-enter="scrollTop" mode="out-in" appear>
   <router-view></router-view>
 </transition>

 methods: {
  scrollTop(){
    document.getElementById('app').scrollIntoView();
  },
}
0
votes

If the default scroll to view does not work, you can achieve the same result with this:

// src/rouer/index.js


[ //routes 
{
  path: '/name',
  name: 'Name',
  component: () => import('../component')
},
.
.
.
]

 createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  scrollBehavior (to, from, SavedPosition) {
    if (to.hash) {
      const el = window.location.href.split('#')[1]
      if (el.length) {
        document.getElementById(el).scrollIntoView({ behavior: 'smooth' })
      }
    } else if (SavedPosition) {
      return SavedPosition
    } else {
      document.getElementById('app').scrollIntoView({ behavior: 'smooth' })
    }
  }
})
0
votes

Why are you returning a promise?
The documentation just returns the position: https://router.vuejs.org/guide/advanced/scroll-behavior.html

So this should be instead:

  scrollBehavior: function(to, from, savedPosition) {
    let position = {}
    if (to.hash) {
      position = {
        selector : to.hash
      };
    } else {
      position = {x : 0 , y : 0}
    }
    return position;
  }

I haven't debugged if to.hash works as you intended, but the function call itself seems incorrect here.