8

I use the window.onhashchange function to execute code when the User changes the hash of the page:

window.onhashchange = function() { /* do something */ };

In some functions I also set the hash via JavaScript:

window.location.hash = "#abc";

I want to prevent the onhashchange event from firing when I set the hash via JavaScript.

What I have tried so far:

var currently_setting_hash = false;

window.onhashchange = function() {
  if (currently_setting_hash)
    return;
 //...
}

currently_setting_hash = true;
window.location.hash = "#abc";
currently_setting_hash = false;

That didn't work because the event is fired with a delay, so the code will first set the hash, then set currently_setting_hash to false and then execute the onhashchange event.

Any ideas how this could be accomplished? Or maybe is there a way to detect if the hash was set by the user or via JavaScript?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Preli
  • 2,953
  • 10
  • 37
  • 50

5 Answers5

11

You could reset the variable from the event handler itself:

var currently_setting_hash = false;

$(window).on("hashchange", function() {
    if (currently_setting_hash) {
        currently_setting_hash = false;
        return;
    }

    currently_setting_hash = false;
    //...
});

currently_setting_hash = true;
window.location.hash = "#abc";
Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
5

Since the event is delayed, there is the possibility of events occurring in a different order than you expect (e.g. imagine the user changing the URL by other means either immediately before or after your code does). It is important to make sure that you do not become inconsistent by assuming the event is yours. Therefore I have a suggestion (based on your code and Adam Bubela's):

var expectedHash;

window.onhashchange = function () {
    if (window.location.hash === expectedHash) {
        return;
    }
    expectedHash = window.location.hash;
    // ... do actual reaction to change here ...
}

function changeHash(hash) {
    hash = canonicalizeHashValue(hash);
    expectedHash = hash;
    window.location.hash = hash;
}

// Helper - return the version of the URL that the browser is expected to
// so that the equality test is accurate.
function canonicalizeHashValue(value) {
    // Borrowing an A element's ability to resolve/parse URLs.
    var tmp = document.createElement('a');
    tmp.href = "";
    tmp.hash = value;
    return tmp.hash;
}

This code will suppress the change handler only if the change is to the value you are expecting. (The assignment inside of onhashchange makes sure that the handler also runs if the hash temporarily goes to another value, which I assume is more correct than the alternative.)

The third helper function canonicalizeHashValue is needed only for precision in case you are specifying a non-canonical value, e.g. changeHash('foo') instead of changeHash('#foo').

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • The `window.location.hash` includes the leading "#" but the `expectedHash` variable doesn't, so shouldn't `expectedHash` be set like `expectedHash = "#" + hash`? – primehalo Feb 05 '16 at 09:20
  • @primehalo: Why do you think the `expectedHash` variable does not contain `#`? – Kevin Reid Feb 05 '16 at 15:23
  • When setting the `window.location.hash` the hash sign doesn't need to be included (w3schools actually says it shouldn't be included) so it might be best to check and if it doesn't exist then prepend it. – primehalo Feb 05 '16 at 19:13
  • @primehalo I see that W3Schools says that, but I cannot think of any justification for that claim. I checked [the WHATWG spec](https://html.spec.whatwg.org/multipage/semantics.html#dom-hyperlink-hash) (generally an adequate guide to what-browsers-(should-)actually-do) and it explicitly adds and removes the "#" such that setting a value without "#" is exactly equivalent to one with it. – Kevin Reid Feb 07 '16 at 18:50
  • Yes, the `window.location.hash` setter accepts a value without the "#" and then the getter always returns a value with the "#". So someone can pass in a hash string like "my-hash" to `changeHash()`, setting `expectedHash` to "my-hash", which will then be compared against "#my-hash" from `window.location.hash`. And those won't match because one has the "#" and one doesn't. That's why I think `changeHash` should add the "#" to `expectedHash` if it doesn't already exist. – primehalo Feb 08 '16 at 09:38
  • @primehalo I think it's reasonable to say "The parameter to this function should always start with #" and that it makes a simpler explanation of the principle, but it's also reasonable to want robustness in the presence of imprecise input, so I've changed the code. – Kevin Reid Feb 08 '16 at 15:30
3

If you want to use just plain Java Script:

    var currently_setting_hash = false;

    window.onhashchange = function() {
        if (currently_setting_hash){
            currently_setting_hash = false;
            return;
        //...
        }
        alert('outside the script');
    }
    function changeHash(hash){
        currently_setting_hash = true;
        window.location.hash = hash;
    }
Adam Bubela
  • 9,433
  • 4
  • 27
  • 31
1

Now You could use the History.replaceState() method. It replaces the hash without triggering the "onhashchange".

window.onhashchange = function() { /* do something */ };

// ... 

history.replaceState(undefined,undefined,'#1234');  // This will replace the hash without triggering 'onhashchage'.
guest
  • 11
  • 1
0

Well, since the event is delayed, if you change hash (for any reason) more than once, then more events will fire in a row. In such case you should increment an integer every time hash is changed.

var setting_hash = 0;

window.onhashchange = function() {
    if (setting_hash){
        setting_hash--;
        return;
    }
    //code here
}
function changeHash(hash) {//hash without '#'
    if (hash!=window.location.hash.substr(1)) {
      setting_hash++;
    }
    window.location.hash = hash;
}