Reputation: 527
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
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
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
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
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.
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
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
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
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
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
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:
<router-view>
transition start. New content NOT YET mounted<router-view>
correctly mounted/renderedWithout 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
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
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
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
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
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
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
Reputation: 1127
This can work too if jQuery is available:
scrollBehavior (to, from, savedPosition) {
$('#selector-in-element-with-scroll').scrollTop(0)
}
Upvotes: -3
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