Similar to the way you can define a scrollBehavior function on a router instance, I need a way to define how the savedPosition value gets computed/populated. I need to override the default behavior which stores the window's scroll position, and instead store the scroll position of another element.
My application uses a structure where the document/window stays static, and content scrolls inside of a container. Example: http://codepen.io/brad426/pen/pezRge
Hi Brad.
just thinking out loud:
I think this won't be so easy to do since those two things - the scrollposition the router saves right now, and the scrollpostion of an arbitrary element - have not much in common, technically. One works with the popstate event and push() method of window.history, while the other requires to read and write to a certain DOM elements property.
Also, this functionality is not necessarily tied to router components, but could be useful to all sorts of components, which makes me think that it would be better to do this in a generic plugin/component ...
The scrollBehavior could be an object instead of a function but as @LinusBorg I'm wondering if keeping this behaviour in a component isn't more flexible and reusable
Cool, thanks for your feedback.
I agree that this functionality would be useful to all sorts of components, but I think a lot of people might also find a "global" override useful.
Regarding my example app structure, I'd like to keep the router's functionality exactly as is, i.e. have it call saveScrollPosition whenever a popstate event is triggered, the only change would be to allow a custom handler for saveScrollPosition . Example:
const router = new VueRouter({
mode: 'history',
routes,
saveScrollPosition() {
let el = document.querySelector('.page-content')
return { x: el.scrollLeft, y: el.scrollTop }
}
})
@LinusBorg see my issue's program #1249 , is that possible to realize?
IMO, we should set a scrollEl property in main router config. Thus allowing us to use a specific scroll container for both scrollBehavior and scroll position.
What's everyone's opinion on this?
You don't need special functions for this, before/afterEach would be a much better place to put these kinds of things
It would be nice if we could somehow define from which element we want to save the scroll position.
In the meantime i solved it this way:
<keep-alive>
<router-view></router-view>
</keep-alive>
const router = new VueRouter({
mode: 'history',
routes
})
const scrollableElementId = 'content' // You should change this
const scrollPositions = Object.create(null)
router.beforeEach((to, from, next) => {
let element = document.getElementById(scrollableElementId)
if (element !== null) {
scrollPositions[from.name] = element.scrollTop
}
next()
})
window.addEventListener('popstate', () => {
let currentRouteName = router.history.current.name
let element = document.getElementById(scrollableElementId)
if (element !== null && currentRouteName in scrollPositions) {
setTimeout(() => element.scrollTop = scrollPositions[currentRouteName], 50)
}
})
I think this would be a pretty good and obvious feature to add :D
@blake-newman I disagree, I think that each router-view (component) should define it's scrollEl with a local scrollEl property on the component, or a v-scrollEl directive in html.
In terms of encapsulation, each component could be from a different source, as an app could be composed of many separate components from different authors on the web. Each component know's which child element within needs to scroll. It would be inappropriate to let the app decide what element within all components should be scrollable (as you would have to go into each component and add the scroll class/id).
If anything, vue should implement an html directive ie: <div v-scrollEl></div> that vue automatically picks up to override scroll position tracking for that component only.
This way it:
@yyx990803 Does vue-router save scroll position of the <router-view> or window?
Also, it seems that vue-router should NOT implement this at all...scrolling a container that is not supported has unintended side-effects ex: safari scroll to top doesnt work, mobile pull to reload can get trapped, scrolling can get trapped by an outer element and require two taps to scroll, etc.
Those are all unfortunate side-effects, but mobile browsers ignoring overflow: hidden on body sometimes forces us to use another container for scrolling, since the only way around that is setting a wrapper element to position: fixed
On mobile browsers, the browser url bar/back buttons (think mobile safari/chrome) are hidden according to scroll position on the window/body. If you use overflow: hidden; on the window/body then you have just disabled the browsers ability to show/hide its controls as well as the many other things I mentioned. What I am saying, really, is that you should avoid disabling scrolling on the body and using position: fixed; to create your own viewport, and try to find another way/use routing or conditionals with transitions if you need to bring in overlays etc.
Any update on this? I want to catch if the savedPosition has been set, and if possible read out the x/y and overrule this. Thanks!
Just as a reference, after some head scratching, here is a pure vue solution (no jquery needed)
In the root component, give the scrolling element a ref property, and pass a scroll_last_position prop to the router-view, containing a callback that restores the last scroll position.
<div ref="scrolled">
<keep-alive>
<router-view :scroll_last_position="scroll_container"></router-view>
</keep-alive>
</div>
Hook in a afterEach route guard so the root element knows when navigation happens. In that route guard, capture the current scroll position of the referenced element and store it for the from route, then restore the old position of the to route.
Also, provide a method for restoring a scroll position.
(This is coffeescript, sorry. I'm confident you can deal with it)
root_component =
data: () ->
scroll: 0
scroll_positions: {}
created: () ->
this.$router.afterEach (to, from) =>
# On each router change, note the current scroll position of the
# container element for the old route; then change the scroll
# position to the last remembered position of the new route, if
# any.
# The child element then can trigger scrolling to that position
# by calling its "on_activate" prop.
this.scroll_positions[from.name] = this.$refs.scrolled.scrollTop
old_pos = this.scroll_positions[to.name]
this.scroll = if old_pos then old_pos else 0
methods:
scroll_container: () ->
this.$refs.scrolled.scrollTop = this.scroll
Finally, in the child component, restore the scroll position if wanted:
child_component =
props:
scroll_last_position:
type: Function
activated: () ->
this.scroll_last_position()
Just wanted to "encapsulate" @mjl code, considering that this should be implemented in the near future keeping everything together should be easier further down the line.
For any routes you want to remember the position, just add the route name to the remember array.
<template>
<div class="container-with-scrollbar" ref="scrolled">
<router-view></router-view>
</div>
<template>
<script>
export default {
data(){
return {
scroll_positions: {},
remember: []
}
},
created(){
this.$router.afterEach( (to, from) => {
this.scroll_positions[from.name] = this.$refs.scrolled.scrollTop;
let scroll = 0;
if(this.scroll_positions.hasOwnProperty(to.name) && this.remember.includes(to.name)){
scroll = this.scroll_positions[to.name];
}
this.$nextTick(()=>{
this.$refs.scrolled.scrollTop = scroll;
});
});
}
}
</script>
Hello everyone!
What is a current status? Is it implemented? Or need use one of custom workaround proposed above?
I save scroll states of a child component by saving $refs.scrollElement.scrollTop on beforeRouteLeave, and restore it at beforeRouteEnter. This requires the
` Bunch of content
`
I'm fairly certain this method could be made into a plugin of some sort. I hope this helped anyone!
A very simple way to do it in TypeScript:
@Ref() readonly content!: Vue; // Points to your scrollable component
scrollPositions: { [index: string]: number }= {};
@Watch("$route")
routeChanged(newRoute: Route, oldRoute: Route) {
const el = this.content.$el;
this.scrollPositions[oldRoute.path] = el.scrollTop;
this.$nextTick(() => (el.scrollTop = this.scrollPositions[newRoute.path]));
}
contain this mixins in your router page.
export default {
beforeRouteLeave (to, from, next) {
const scroller = this.$refs.scroller;
if(scroller) {
if(!window.__scrollOffset) {
window.__scrollOffset = {};
}
window.__scrollOffset[this.$route.fullPath] = {
x: scroller.scrollLeft,
y: scroller.scrollTop
};
next()
}
},
methods: {
setScroll(scroller) {
if(window.__scrollOffset && window.__scrollOffset[this.$route.fullPath]) {
let { x, y } = window.__scrollOffset[this.$route.fullPath];
scroller.scrollLeft = x;
scroller.scrollTop = y;
}
}
},
}
when your page rendered, calling the function to set scroll position.
This behavior would be incredibly useful to get scrolling working without requiring html history usage. And would render libraries such as https://github.com/jeneser/vue-scroll-behavior unnecessary (at least when using html history mode), since we could just use the vue-router behavior to do such.
As a side note, it would be nice if the window.scrollTo(position.x, position.y) line in scroll.js was overridable so that custom scroll libraries could be used instead of scrollTo.
Extending @iBrazilian2 's solution, you can leverage the savedPosition parameter in scrollBehavior to only navigate to the saved position of your scroll element when navigating back/forward from history (popstate). This seems the most simple solution for default history behavior in vue with a scrollable element that isn't window:
// router.js
const scrollableElementId = 'id-of-your-scrollable-element'; // change id
const scrollPositions = Object.create(null);
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior(to, from, savedPosition) {
const element = document.getElementById(scrollableElementId);
if (savedPosition && element !== null && to.name in scrollPositions) {
console.log(
'%c%s',
'color:hotpink;',
'scrollBehavior: navigating to history entry, scroll to saved position',
);
element.scrollTop = scrollPositions[to.name];
} else {
console.log('%c%s', 'color:hotpink;', 'navigating to new history entry, scroll to top');
element.scrollTop = 0;
}
},
});
router.beforeEach((to, from, next) => {
const element = document.getElementById(scrollableElementId);
if (element !== null) {
scrollPositions[from.name] = element.scrollTop;
}
next();
});
Here's my solution:
<script>
const savedPosition = {
x: 0,
y: 0,
}
export default {
mounted () {
this.$refs.scrollable.scrollTo({
left: savedPosition.x,
top: savedPosition.y,
})
},
beforeDestroy () {
savedPosition.x = this.$refs.scrollable.scrollLeft
savedPosition.y = this.$refs.scrollable.scrollTop
},
}
</script>
the original suggestion would be a good use case to add, am wondering if this is still being considered?
Most helpful comment
It would be nice if we could somehow define from which element we want to save the scroll position.
In the meantime i solved it this way: