Can't get scrollBehavior working

I can’t seem to get scrollBehavior working at all

const scrollBehavior = (to, from, savedPosition) => {
  if (savedPosition) {
    return savedPosition
  }
  
  else {
    position.x = 0
    position.y = 0
  }
  
  return position
}

const router = new VueRouter({
  scrollBehavior,
  routes: [
    { 
      name: 'one',
      path: '/one',
      component: Page,
      meta: { depth: 1 }
    },
    ...

Here’s a CodePen https://codepen.io/chasebank/pen/jzNByK

The only thing that works is

router.afterEach(() => {
 setTimeout(() => {
   window.scrollTo(0, 0);
 }, 600)
});

But that obviously breaks the scroll position when using the browsers back button…

Any advice would be greatly appreciated, thanks!

At first I thought it was because you’re using absolute positioning on your app element. As scrollBehavior targets the root node it won’t have anything to scroll (as technically it would be 0). But, the fact that scrollBehavior doesn’t fire at all is really odd. Do you get the same behavior in your actual code (scrollBehavior never being fired)?

1 Like

Thanks for taking a look! It’s always bitter-sweet when your confusion if validated by someone else

I thought the same about the positioning… originally I wasn’t using a transition mode, so that the in and out routes transitioned together, and would obviously need absolute positioning in that case… But I thought the positioning was causing all the problems, so changed to an out-in mode. But still having issues…

Do you get the same behavior in your actual code (scrollBehavior never being fired)?

You mean, am I getting this in my actual project, as opposed to just this reduced test case? I haven’t gotten that far yet :sweat_smile:

I was actually also abandoning Nuxt for similar transition/scroll issues I was having… I thought sticking with vanilla vue would eliminate the issue but now I’m sort of back where I left off

Reason I ask, is perhaps there’s something going on with CodePen and the routing behaviour. Maybe try to replicate it on https://codesandbox.io/s/vue as it actually runs a build process.

1 Like

First of all, I don’t think I’ve seen CodeSandbox before… very nice…

Second, you may be right. I can’t think of anything that would cause a difference on CodePen though. The only weird things I’ve seen are issues caused by the iframe in the editor, but there’s no iframe in the debug view.

But on CodeSandbox, I’m finally getting some action in the console: ReferenceError: position is not defined

So, that’s a good sign. I’m assuming that means scrollBehavior is indeed firing. Still stuck though

Ah, wait… so that actually is working. I don’t know where I got that previous scrollBehavior example from. Somewhere along the way of throwing everything I could find at the problem…

But I changed it to

const scrollBehavior = (to, from, savedPosition) => {
  if (savedPosition) {
    return savedPosition;
  } else {
    return { x: 0, y: 0 }
  }
};

and that works! But it scrolls before the page transitions.

I also tried this, but the delay doesn’t seem to work

const scrollBehavior = (to, from, savedPosition) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ x: 0, y: 0 })
    }, 600)
  })
}

Edit… just kidding lol that does work! Something needed to refresh on codesandbox. Going back from the browser (in the editor view) doesn’t load the saved position though. So the perfect solution would be some combination of those two.

Either way though, this is now way better than what I was experiencing before, so I appreciate your help!

Happy to hear it’s working :slight_smile: If you want to tie into the hook after the transition you need trigger the scrolling based on the event.

Something like this:

In your template:

<transition name="fade" mode="out-in" @after-enter="afterEnter" appear>
  <router-view></router-view>
</transition>

App.vue

afterEnter () {
  this.$root.$emit('scrollAfterEnter');
},

Router

import app from './app';

// within scrollBehavior
return new Promise(resolve => {
  app.$root.$once('scrollAfterEnter', () => {
    resolve(position);
  });
});
1 Like

Again, thanks so much for your help, James

With some observations and tinkering, I was able to get both savedPosition and scroll to top working in the setTimeout version

return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (savedPosition) {
          resolve(savedPosition);
        } else {
          resolve({ x: 0, y: 0 });
        }
    }, 600);
  });

As for what you’ve posted, that looks nifty, and could avoid the hard coded timeout… and I assume could also look like

  return new Promise(resolve => {
    app.$root.once("scrollAfterEnter", () => {
      if (savedPosition) {
        resolve(savedPosition);
      } else {
        resolve({ x: 0, y: 0 });
      }
    });
  });

But any idea why I’m getting Cannot read property '$once' of undefined?

Sorry, that was a bit my fault. I copied it from some of my project code… Within my app.js (not app.vue) I declare my Vue instance and assign it to a constant. Then I export it for use within my router… here’s a working fork https://codesandbox.io/s/yvor6vyyw9

You’ll notice I’ve replaced app with main to try and avoid confusion in your sandbox.

1 Like

@JamesThomson Sorry but are you sure that’s fixed? I see the same Cannot read property '$once' of undefined error when changing routes.

I noticed something interesting with my setTimeout method in my local project. It works when navigating forward, but does not work when navigating back using the browser button.

I don’t see this behavior in codeSandbox, but I also can’t really test it there. The back button works in the editor view, with the sort of “fake” browser", but in the full-page view, the browser history buttons don’t work at all. https://m9omkmo1y9.codesandbox.io/#/

I’m assuming this is something related to codeSandbox.

At any rate, to describe the problem I’m seeing. When I navigate to a page, the setTimeout delays scroll change for .6 seconds (the duration of the transition). When I navigate back using the browser button, the savedPosition works, but the change happens instantly.

Perhaps your method would fix this issue though, since it’s not relying on the timeout :pray:

Whoops. Forgot to hit the Save button. One downside of Codesandbox :stuck_out_tongue: Try it out now. Back buttons, including browser, should be working.

1 Like

Yep! That did it!

Still see two slight issues though :laughing: The first was with after-enter the scroll happens after the transition has completed entirely.

vue-scrolling-after-enter

I think that can be fixed by switching to before-enter or even just enter?

I forked yours, with that change, and added more content so it’s easier to scroll https://codesandbox.io/s/2wo6lvr97j

That results in:
vue-scrolling-before-enter

Which looks great! but in both cases, the scroll on the back nav happens instantly (before the current route has a chance to leave). I have a hunch that this has something to do with how browsers handle history, and is maybe unavoidable?

Yeah, could be a browser thing. I’m not seeing what your gif shows. As far the after-enter it depends on how your content loads. In the app I’m working on the page transitions and then the content loads through xhr and then is injected which means the scroll needs to happen after the content exists otherwise the scroll position is off. Leads to a bit of an annoying jump, but I’m not sure if there’s any way around that - aside from reworking the app to load content on the router level.

1 Like

Yeah, I think it is a browser thing. Not too big of a deal

For the after/before, that’s interesting! I hadn’t considered the impact of how/when you’re getting content. Which tells me I’ve pushed this about as far as I can for a generalized prototype

Thanks so much for your help and time! I Definitely learned some stuff

No worries, glad I could help. Happy coding!

Sorry to reopen this ticket but I just figured out that mode=“out-in” is the culprit. I went back and forward on my code to figure out what was giving me the same results as you (worked for me for a while and suddenly broke). And when looking at your code I can see you use it too. I removed mode=“out-in” on the router transition then savedPosition and scrollBehavior started working as expected. No other changes made.

1 Like

I found the CodeSandbox very helpful in making scrollBehavior work.

I had an issue though with cyclical dependency from importing the root Vue instance into the routes file.

In the CodeSandbox (https://codesandbox.io/s/yvor6vyyw9), inside app.js, main is exported and router is imported. This causes a cyclical dependency when main is imported into router/index.js.

I found a solution for this cyclical dependency issue though, and I will explain it with respect to the CodeSandbox sampler pack.

In app.js, I changed nothing.

In routes.js, I replaced main.$root.$once() with this.app.$once(). From the perspective of the scrollBehavior function in router, this.app refers to the root Vue instance. You can probe it by console.logging this.app.

In App.vue, I changed nothing. It still calls this.$root.$emit().

Here is my scrollBehavior function, for posterity:

    scrollBehavior(to, from, savedPosition) {
        return new Promise(resolve => this.app.$once('scrollAfterEnter', () => {
            if (savedPosition) {
                return resolve(savedPosition);
            }

            return resolve({ x: 0, y: 0 });
        }));
    },

I left it slightly-verbose like that just in case I need to patch more custom logic into here later. Maximum terse ES6 syntax was a bit too concise and frankly, hard to read.