Option 1: Listening to Changes in URL
As SPAs use routing, you could ideally listen to url changes.
One crude way of doing this is to keep polling using setInterval and track the changes to the url. If the url has changed you could run your handler. However this is wasteful.
Given the lack of options you can listen to all events which could cause transitions, (e.g. mouse clicks, keyboard Enter/Space key press etc.) and in the handler check if the url has changed. Use requestAnimationFrame so that we don't execute handler prematurely.
let currentUrl = location.href;
const checkPageTransition = () => {
requestAnimationFrame(() => {
if (currentUrl !== location.href) {
console.log("url changed");
}
currentUrl = location.href;
}, true);
};
document.body.addEventListener("click", checkPageTransition);
document.body.addEventListener("keyup", e => {
if (e.code === "Enter" || e.code === "Space") checkPageTransition()
});
Option 2: Listening to popstate event
If you are using modern SPAs, they would most likely be using history.pushState and history.popState to manage routing. We could then listen to the window.popstate event. But there are limitations to this event.
From: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
Note: Calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript), when navigating between two history entries for the same document.
However we can monkey patch the history.pushState function so that it always fires the history.onpushstate function.
(function(history) {
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function")
{
history.onpushstate({
state: state
});
}
return pushState.apply(history, arguments);
}
})(window.history);
You would need to similarly patch the replaceState function as well.