dippy tippy
dippy tippy

Reputation: 527

Scroll behaviour VueJS not working properly

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 ?

Upvotes: 14

Views: 30717

Answers (16)

Stefan Drazic
Stefan Drazic

Reputation: 11

A little improvement on what @Masoud Ehteshami wrote, I implemented TypeScript and timeout

scrollBehavior(to, from, savedPosition) {
if (to.hash) {
  const el = window.location.href.split("#")[1];
  if (el.length) {
    setTimeout(() => {
      document.getElementById(el)?.scrollIntoView({
        behavior: "smooth",
      });
    }, 100);
  }
} else if (savedPosition) {
  return savedPosition;
} else {
  document
    .getElementById("app")
    ?.scrollIntoView({ behavior: "smooth" });
} }

Upvotes: 0

agm1984
agm1984

Reputation: 17178

I was having issues with this because of my layout. For me it wasn't the page or app container, or any container up in that root area. It was an inner page div that's height was calculated to fill the viewport, so the scrollbar was in that container.

I solved it by using (on that inner page div) CSS attribute:

scroll-padding-top: 99999px;

https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding-top

Usage of the large number causes the scrollbar to always be at the top of the div. My problem was the scrollbar was scrolled halfway down every time you reload the page, so this fixes it.

I read something that indicated if you need to update the padding value after loading, you may encounter issues, but that doesn't occur in my app.

There is also scroll-margin-top which could be similarly useful.

[edit]: I found the true problem to my issue. I noticed that scroll-padding-top caused the inner-page to scroll back to the top when I clicked on a checkbox near the bottom, so the scroll-adding-top solution was not-viable in my use case.

I noticed that the problem occurred on one of my pages but not another, and a key difference was that one was calling an el.focus() to focus an input element on page-load. Removing that got rid of the "being scrolled down" on page-load issue.

After more analysis, I fixed it by adding the preventScroll option to el.focus():

document.querySelector('#some-input').focus({ preventScroll: true });

Upvotes: 0

ufa9
ufa9

Reputation: 29

Thanks to someone in another question , I decided to try the "delaying scroll" described here https://router.vuejs.org/guide/advanced/scroll-behavior.html#delaying-the-scroll

and it's working ! I copied it from the documentation, the code :

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ left: 0, top: 0, behavior: 'smooth' })
      }, 500)
    })
  },
})

Actually my problem was that when I used the syntax "return {}" it was working, but on mobile, it was not working at all. So I tried this, it works. And SMOOTH can be added !

Instead of writing 500 (ms), just write 0 if you want it to do it directly and it works fine. Finally something that works

Upvotes: 0

Lars Schellhas
Lars Schellhas

Reputation: 359

As @Xth pointed out, a lot of the confusion around this topic comes from the fact that version 3 and 4 of vue-router handle the scrollBehaviour parameters differently. Here are both ways.

vue-router 4

scrollBehavior (to, from, savedPosition) {
    if (to.hash) {
      return {
        // x, y are replaced with left/top to define position, but when used with an element selector (el) will be used as offset
        el: to.hash,
        // offset has to be set as left and top at the top level
        left: 0,
        top: 64
      }
    }
  }

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

vue-router 3

scrollBehavior (to, from, savedPosition) {
    if (to.hash) {
      return {
        // x, y as top-level variables define position not offset
        selector: to.hash,
        // offset has to be set as an extra object
        offset: { x: 0, y: 64 }
      }
    }
  }

Official documentation V3: https://v3.router.vuejs.org/guide/advanced/scroll-behavior.html

Upvotes: 4

Dave
Dave

Reputation: 1429

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.

Upvotes: 1

Farnaam Samadi
Farnaam Samadi

Reputation: 215

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.

EDIT 3/8/2022:

Now everything is fine with the documentation.

Upvotes: 20

typefox09
typefox09

Reputation: 81

For anyone else that is having this issue, I found removing overflow-x-hidden on the primary container solves the issue.

Upvotes: 0

Jordan Kowal
Jordan Kowal

Reputation: 1594

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 };
  }
}

Upvotes: 4

Masoud Ehteshami
Masoud Ehteshami

Reputation: 151

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" });
    }
  },
});

Upvotes: 10

Sweet Chilly Philly
Sweet Chilly Philly

Reputation: 3219

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.

Upvotes: 15

Peter Pointer
Peter Pointer

Reputation: 4170

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.

Upvotes: 0

Gblend
Gblend

Reputation: 11

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' })
    }
  }
})

Upvotes: 0

LastM4N
LastM4N

Reputation: 2240

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();
  },
}

Upvotes: 0

Bruno Tavares
Bruno Tavares

Reputation: 462

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().

Upvotes: 3

Ryan Charmley
Ryan Charmley

Reputation: 1127

This can work too if jQuery is available:

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

Upvotes: -3

jonnycraze
jonnycraze

Reputation: 498

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 }
    }
  }
}

Upvotes: -1

Related Questions