5

The documentation for the popstate Window event states:

Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript).

I need a way to execute some code when the URL changes by means that don't trigger the popstate event (such as history.replaceState()). Preferably, using the MutationObserver API[1], and most definitely without polling the URL every x seconds.

Also, hashchange won't do, as I must act on every change in the URL, not just the hash part.

Is that possible?


[1] It is explained on another question why the MutationObserver API doesn't work for this.


(other) Related questions:

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
  • just call your other code when you call replaceState(), or create a custom event and dispatch it. if code you don't control calls it, wrap the function to achieve the last sentence. – dandavis Jun 25 '19 at 19:26
  • But I'm not the one calling `replaceState()` and similar functions. The web application code is. (I'm writing an userscript). – Marc.2377 Jun 25 '19 at 19:27
  • can't you run the userscript before the other code loads? most extensions provide that as a setting. – dandavis Jun 25 '19 at 19:28
  • You could continuously poll in an interval but you don't want that state running/consuming in the background the entire time? (just thinking) – Ilan P Jun 25 '19 at 19:29
  • @dandavis Yes, I'm currently doing that, but the script is only loaded once (when the page is first loaded). If the application changes its own URL, my script must detected it, and that's what I'm after. – Marc.2377 Jun 25 '19 at 19:30
  • @IlanP Precisely. I could do polling, but 1) the URL change may never happen in the first place, and 2) if it does happen, I want to act *immediately*, so the polling interval should be very very low and the resource usage would be a problem. Events (or Observers perhaps) should be the most professional way. I figured I'd ask how to implement it, because nobody did (except [a guy on Reddit](https://www.reddit.com/r/learnprogramming/comments/7vjcr0/javascript_is_there_a_url_change_observer/), with no satisfactory answer). – Marc.2377 Jun 25 '19 at 19:32
  • Gotcha, one approach (this is more logic than code, but is not full proof because you honestly cannot predict what the end-user does) is by tracking the cursor location (similar to how Facebook tracks cursor coordinates); knowing when the mouse enters and exists a certain zone you somewhat have an idea to begin an interval and poll (with a timeout to stop after a certain amount of time); again just thinking out of the box :) – Ilan P Jun 25 '19 at 19:38
  • @IlanP: my mouse has a back button on the side... – dandavis Jun 25 '19 at 19:39
  • @dandavis 10000%, not full proof but just an idea.. you're absolutely right though; third-mouse button/abrupt closing, etc won't get calculated; but it's an idea; – Ilan P Jun 25 '19 at 19:41
  • there's a trivial solution that works 100% for the OP's described problem, no need for creativity this time.;) but i like your spirit. keep up the good fight... – dandavis Jun 25 '19 at 19:42
  • @dandavis gotcha; ok – Ilan P Jun 25 '19 at 19:43
  • Actually, MutationObserver on the `` and/or body is often a good-enough proxy. – Brock Adams Jun 27 '19 at 02:24
  • Unfortunately, dandavis' script doesn't work 100% on Facebook. Only solution seems to be observing the body element: https://stackoverflow.com/a/46428962/1287812 – brasofilo Oct 22 '20 at 23:27
  • @brasofilo can you elaborate on why dandavis' script doesn't work 100% on Facebook? What happens? I'm curious why it wouldn't work. (Assuming it's also augmented to do the same monkeypatch for history.pushState and listen to popstate and hashchange events as well). – Hans Brende Nov 04 '22 at 17:45

1 Answers1

9

You need to redefine (wrap) the history.replaceState that the other script is using:

(function(){
  var rs = history.replaceState; 
  history.replaceState = function(){
    rs.apply(history, arguments); // preserve normal functionality
    console.log("navigating", arguments); // do something extra here; raise an event
  };
}());

No polling, no waste, no re-writing, no hassle.

Do the same for pushState, or whatever other natives (like ajax) you wish/need when writing userscripts.

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
dandavis
  • 16,370
  • 5
  • 40
  • 36
  • The OP would need to do the same with `pushState`, and something similar with `location`, `location.href`, etc. – Heretic Monkey Jun 25 '19 at 19:39
  • 1
    @HereticMonkey: no, changing `location.href` would reload the page and destroy the script variable anyway (though the user script would still fire on the page load then, as usual). i mentioned pushState in the answer... – dandavis Jun 25 '19 at 19:40
  • Yeah, I'm kind of surprised no one noted that in the question. I can also just type in the address bar... – Heretic Monkey Jun 25 '19 at 19:41
  • I'll be back in a couple hours and test this; thanks for answering – Marc.2377 Jun 25 '19 at 19:44
  • np. it's not a pattern i would use in a lib on production code, because of remotely possible perf or compat issues, but for a user-script it's perfect. when JS doesn't provide what you want, sometimes you have to cheat... – dandavis Jun 25 '19 at 19:50
  • Upvoted as this works for my use case right now (although a bit ugly to implement from the userscript - see [here](https://stackoverflow.com/q/13485122/3258851)). Will leave it open a while longer, to see what else might come up, even though I'm inclined to agree that there is no "native" way (so to speak). – Marc.2377 Jun 26 '19 at 05:05
  • do you really have to go through that old extension interface? I write my userscripts as if they were plain scripts and they seem to work fine. i use tampermonkey though. If i was faced with all that boilerplate, i would just use the userscript to inject in an external script written in "plain" js. – dandavis Jun 26 '19 at 17:08
  • As I'm writing an userscript that _must_ be compatible with GM on Firefox, unfortunately, yes. – Marc.2377 Jun 26 '19 at 20:28
  • @dandavis can you edit your answer to do the same monkeypatch for history.pushState, plus listen for popstate and hashchange events? Reason being, some comments are saying your answer doesn't work 100% and now I'm not sure if that's because they just neglected the other possible events, or because there was something deeper going on to prevent it from working. – Hans Brende Nov 04 '22 at 17:48