vuejs / vue-router

🚦 The official router for Vue 2
http://v3.router.vuejs.org/
MIT License
18.99k stars 5.06k forks source link

Navigation to the same route fails (anchor, hash) #1668

Closed ParachuteCat closed 3 years ago

ParachuteCat commented 7 years ago

Version

2.4.2

Reproduction link

https://jsfiddle.net/ParachuteKadse/pL0sehq5/show/

Steps to reproduce

  1. Click on Foo
  2. Click Foo-3
  3. Scroll up
  4. Repeat step 2

What is expected?

Scroll down to Foo-3, like the first time.

What is actually happening?

Screen stays at the same position and doesn't scroll to Foo-3.

Special notes

You can see the correct URL in the status-bar of your browser (Tested on Chrome)

svewag commented 7 years ago

I made a workaround by using the method $router.push instead of the router-link component.

This method takes a success callback (in this case irrelevant) as 2nd and a abort callback as 3rd parameter. $router.push(route, success, abort)

These callbacks will be called when the navigation either successfully completed (after all async hooks are resolved), or aborted (navigated to the same route, or to a different route before current navigation has finished), respectively.

see https://router.vuejs.org/en/essentials/navigation.html

speir-wang commented 6 years ago

@ParachuteKadse Hey mate, I came across the same problem, have you solved this one? If so, would you mind sharing your solution here?

Cheers

CharlesKumar commented 6 years ago

savedPosition, is only available if this is a popstate navigation (triggered by the browser's back/forward buttons). The object could be in the form of:

If a falsy value or an empty object is returned, no scrolling will happen. read more

he popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript)... read more

savedPosition is meant for save the scroll position object when using back in browser. (AFAIK)

official example also suggests use hash for anchor behavior

https://github.com/vuejs/vue-router/blob/dev/examples/scroll-behavior/app.js

Your expected behavior can be achieved by vue-scroll plugins

or use standalone solution using #ids with libraries like jump, zenscroll or other similar libraries

sem4phor commented 6 years ago

Possible workaround:

this.$router.push({ name: 'home' }, undefined, () => { location.href = this.$route.hash }) As the 3rd argument is the abort() function, it may have unwanted side effects though..

If you want to use it globally, add a function to your Router:

pushWithAnchor: function (routeName, toHash) {
    const fromHash = Router.history.current.hash
    fromHash !== toHash || !fromHash
    ? Router.push({ name: routeName, hash: toHash })
    : Router.push({ name: routeName, hash: fromHash }, undefined, () => { window.location.href = toHash })
  }

And use it in components with: this.$router.options.pushWithAnchor('home', '#fee-calculator-section')

aldencolerain commented 6 years ago

I think it would be fantastic to improve the anchor support. I ran into quite a few questions and issues around this. After reading what everyone has written above I thought I'd paste my solution. I created an anchor-link component composed of a router-link that is used in conjunction with the scrollBehavior function. I haven't thoroughly tested it but here is the basic idea (I think this is the same route check could be improved):

anchor-link.vue

<template>
  <span @click="navigate"><router-link ref="link" :to="to"><slot></slot></router-link></span>
</template>

<script>
  export default {
    name: 'anchor-link',
    props: ['to'],
    methods: {
      navigate() {
        const current = this.$router.currentRoute.fullPath;
        const href = this.$refs.link.$el.getAttribute('href');
        if (current === href) {
          location.href = this.to.hash;
        }
      }
    }
  };
</script>

router.js excerpt

function scrollBehavior(to, from, savedPosition) {
  if (to.hash && document.querySelector(to.hash)) {
    return { selector: to.hash };
  }
  if (savedPosition) {
    return savedPosition;
  }
  return false;
}

const router = new Router({ mode: 'history', routes, scrollBehavior });

None of these solutions really work on reloading the page I tried stuff like below but there obviously a race condition, it doesn't really work. Not sure how to come up with a good solution for anchor pages on reload. I feel like we might be competing with the browser's default behaviour. Maybe someone with more experience could lend some insight?

    mounted() {
      const anchor = this.$router.currentRoute.hash;
      this.$nextTick(() => {
        if (anchor && document.querySelector(anchor)) {
          location.href = anchor;
        }
      });
    }
gspain commented 6 years ago

I gave up on hacks and workarounds. I've tracked this issue down to lines 1916-1923 in vue-router.js. Commenting the if statement out seems to work, though I'm sure it would be better if it were updated to support same hash navigation.

if (
  isSameRoute(route, current) &&
  // in the case the route map has been dynamically appended to
  route.matched.length === current.matched.length
  ) {
  this.ensureURL();
  return abort()
}
ranaclyde commented 6 years ago

The @aldencolerain 's implementation worked perfectly for me

    mounted() {
      const anchor = this.$router.currentRoute.hash;
      this.$nextTick(() => {
        if (anchor && document.querySelector(anchor)) {
          location.href = anchor;
        }
      });
    }
SilverDragon135 commented 6 years ago

I went with @gspain solution. I didn't noticed any implications yet (few weeks).

ajb413 commented 6 years ago

This worked for me: My Vue Router uses the default mode (hash). The way I see it, because my URLs have 2 hash symbols, the browser behavior of scrolling to a #some-id at the end of the URL breaks. I used code from earlier in this thread to make my own fix that only restores the scrolling feature, instead of changing the history stack. Also, my default scroll behavior is to always begin at the very top.

This works whether you inter-site link, navigate directly using the address bar, or refresh the page.

router/index.js

const router = new Router({
    routes: [
        // route objects here
        // ...
    ],
    scrollBehavior(to, from, savedPosition) {
        return {
            x: 0,
            y: 0,
        };
    },
});

main.js

import Vue from 'vue';
import App from './App';
import router from './router';

Vue.config.productionTip = false;

const fixIdScrolling = {
    watch: {
        $route(to, from) {
            const currentRoute = this.$router.currentRoute;
            const idToScrollTo = currentRoute.hash;
            this.$nextTick(() => {
                if (idToScrollTo && document.querySelector(idToScrollTo)) {
                    document.querySelector(idToScrollTo).scrollIntoView();
                }
            });
        },
    },
};

/* eslint-disable no-new */
new Vue({
    mixins: [fixIdScrolling],
    el: '#app',
    router,
    components: {App},
    template: '<App/>',
});
vinstah commented 5 years ago

This worked for me: My Vue Router uses the default mode (hash). The way I see it, because my URLs have 2 hash symbols, the browser behavior of scrolling to a #some-id at the end of the URL breaks. I used code from earlier in this thread to make my own fix that only restores the scrolling feature, instead of changing the history stack. Also, my default scroll behavior is to always begin at the very top.

This works whether you inter-site link, navigate directly using the address bar, or refresh the page.

router/index.js

const router = new Router({
    routes: [
        // route objects here
        // ...
    ],
    scrollBehavior(to, from, savedPosition) {
        return {
            x: 0,
            y: 0,
        };
    },
});

main.js

import Vue from 'vue';
import App from './App';
import router from './router';

Vue.config.productionTip = false;

const fixIdScrolling = {
    watch: {
        $route(to, from) {
            const currentRoute = this.$router.currentRoute;
            const idToScrollTo = currentRoute.hash;
            this.$nextTick(() => {
                if (idToScrollTo && document.querySelector(idToScrollTo)) {
                    document.querySelector(idToScrollTo).scrollIntoView();
                }
            });
        },
    },
};

/* eslint-disable no-new */
new Vue({
    mixins: [fixIdScrolling],
    el: '#app',
    router,
    components: {App},
    template: '<App/>',
});

Thanks heaps this should be implemented

pkkid commented 5 years ago

The changes from @ajb413 mostly work, except when reloading the page or navigating from an external site. Anyone else having those issues?

RobertDiebels commented 5 years ago

@pkkid Having the same issue. Both scrollBehavior and fixIdScrolling only trigger when the app is changing a route not when the Vue application is first entered nor when the route remains the same.

Personally I'd love to see some functionality that allows me to override that behavior.

moh1434 commented 4 years ago

The @aldencolerain 's implementation worked perfectly for me

    mounted() {
      const anchor = this.$router.currentRoute.hash;
      this.$nextTick(() => {
        if (anchor && document.querySelector(anchor)) {
          location.href = anchor;
        }
      });
    }

This work with Firefox and mobile(chrome) but does not work with desktop(chrome). chrome(desktop) save the last scroll position and scroll to it after refresh . . Edit: I add history.scrollRestoration = "manual"; inside Then it work with chrome(desktop)

Mohammad-Alavi commented 4 years ago

This is by far the cleanest solution that i have found and it works perfectly.

      methods: {
        goto(refName) {
            var element = this.$refs[refName];
          element.scrollIntoView();
        }
      }

If you wanted to get fancy and make the scroll smooth, you can even add the following:

element.scrollIntoView({ behavior: 'smooth' });

ryanklarhoelter commented 3 years ago

@gspain @SilverDragon135 Is your solution still working for you? It seems that something has changed since your post. At least it's not working for me. Same behavior.

HenrijsS commented 3 years ago

It's been almost 4 years. How has this not been fixed yet???

saadeghi commented 3 years ago

My grandfather had said that one day this bug would be fixed but no one believed him. Here we are after ages...

bluetomlee commented 1 year ago

Already 2023, Any further progress??