Some pages on my application are very, very slow to display, for normal reasons (business intelligence, that calls many external APIs, etc.). Sometimes, 3-5 minutes are needed.
I want to display a progress bar with text, notifying the user what's currently going on and why he's waiting.
Here's how I imagined the feature:
Every time the user displays a page, a new token is generated in the form. When he submits the form, while the browser waits for the response, a route can be called with the given token, that returns an JSON response giving the percentage of progress and the operation currently being done.
So, I wrote a little jQuery code that polls this route, and updates the progress bar as well.
var pollAjax = function () {
this.url = '';
this.interval = 5000;
this.callback = '';
this.running = true;
this.poll = function poll() {
var that = this;
if (that.running) {
$.get(that.url, function (data) {
that.callback(data);
setTimeout(function () {
that.poll();
}, that.interval);
});
}
};
this.setInterval = function setInterval(interval) {
this.interval = interval;
return this;
};
this.setCallback = function setCallback(callback) {
this.callback = callback;
return this;
};
this.setUrl = function setUrl(url) {
this.url = url;
return this;
};
this.stop = function stop() {
this.running = false;
return this;
}
};
$(document).ready(function () {
var progressBars = $('.progress .progress-bar');
progressBars.each(function () {
var progressPoll = new pollAjax();
var progressBar = $(this);
progressPoll.setUrl($(this).data('refreshurl'));
progressPoll.setInterval(2000);
progressPoll.setCallback(function (data) {
if (data.progress) {
$('#progressbar').show();
progressPoll.setInterval(200);
progressBar.attr('data-transitiongoal', data.progress.percentage).progressbar();
$('#progress_text').text(data.progress.text);
console.log(data.progress);
}
});
progressPoll.poll();
});
});
Sorry for the code quality, I'm better with PHP :)
This code works fine. When I fake some data in the poll url, the progressbar correctly updates every 200ms.
My problem is that this code stops working as soon as the user leaves the page (and that's bad because this is when I want the code to be triggered).
When he submits the form, the page is still displayed while the browser is waiting long minutes for the response, but the automatic poll immediately stops. As if the unload event prevents scripts to continue running, even if the page is still displayed.
Do you know how I can handle this ?
Thanks,
Ben
I am trying to display a 'mask' on my client while a file is dynamically generated server side. Seems like the recommend work around for this (since its not ajax) is to use an iframe and listen from the onload or done event to determine when the file has actually shipped to the client from the server.
here is my angular code:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
e.load(function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
});
angular.element('body').append(e);
This works great in Firefox but no luck in Chrome. I have also tried to use the onload function:
e.onload = function() { //unmask here }
But I did not have any luck there either.
Ideas?
Unfortunately it is not possible to use an iframe's onload event in Chrome if the content is an attachment. This answer may provide you with an idea of how you can work around it.
I hate this, but I couldn't find any other way than checking whether it is still loading or not except by checking at intervals.
var timer = setInterval(function () {
iframe = document.getElementById('iframedownload');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Check if loading is complete
if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
loadingOff();
clearInterval(timer);
return;
}
}, 4000);
You can do it in another way:
In the main document:
function iframeLoaded() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
}
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element('body').append(e);
In the iframe document (this is, inside the html of the page referenced by url)
window.onload = function() {
parent.iframeLoaded();
}
This will work if the main page, and the page inside the iframe are in the same domain.
Actually, you can access the parent through:
window.parent
parent
//and, if the parent is the top-level document, and not inside another frame
top
window.top
It's safer to use window.parent since the variables parent and top could be overwritten (usually not intended).
you have to consider 2 points:
1- first of all, if your url has different domain name, it is not possible to do this except when you have access to the other domain to add the Access-Control-Allow-Origin: * header, to fix this go to this link.
2- but if it has the same domain or you have added Access-Control-Allow-Origin: * to the headers of your domain, you can do what you want like this:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element(document.body).append(e);
e[0].contentWindow.onload = function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
};
I have done this in all kinds of browsers.
I had problems with the iframe taking too long to load. The iframe registered as loaded while the request wasn't handled. I came up with the following solution:
JS
Function:
function iframeReloaded(iframe, callback) {
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframeReloaded(iframe[0], function () {
console.log('Reloaded');
})
JQuery
Function:
$.fn.iframeReloaded = function (callback) {
if (!this.is('iframe')) {
throw new Error('The element is not an iFrame, please provide the correct element');
}
let iframe = this[0];
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframe.iframeReloaded(function () {
console.log('Reloaded');
})
I've just noticed that Chrome is not always firing the load event for the main page so this could have an effect on iframes too as they are basically treated the same way.
Use Dev Tools or the Performance api to check if the load event is being fired at all.
I just checked http://ee.co.uk/ and if you open the console and enter window.performance.timing you'll find the entries for domComplete, loadEventStart and loadEventEnd are 0 - at least at this current time:)
Looks like there is a problem with Chrome here - I've checked it on 2 PCs using the latest version 31.0.1650.63.
Update: checked ee again and load event fired but not on subsequent reloads so this is intermittent and may possibly be related to loading errors on their site. But the load event should fire whatever.
This problem has occurred on 5 or 6 sites for me now in the last day since I noticed my own site monitoring occasionally failed. Only just pinpointed the cause to this. I need some beauty sleep then I'll investigate further when I'm more awake.
This is an issue I'm facing only on Chrome.
Code snippets -
// Bind methods to global AJAX events
jQuery(document).bind({
ajaxStart : function() {
showWaitMessage(); // this is where it hangs
},
ajaxStop : function() {
hideWaitMessage();
},
ajaxError : function(jqXHR, exception) {
// error handling
}
});
The location redirect -
var href = "downloadPack?clientName="+clientName+"&clientID="+clientID+"&fundName="+fundName+"&fundID="+fundID+"&navDate="+navDate+"&KD="+KD+"&status="+status;
//setTimeout(function(){document.location.href = href;}, 500);
//window.location.href = href;
jQuery(location).attr('href', href); // Have tried the above two lines too (same issue)
The AJAX call -
function getExceptions() {
jQuery.ajax({url:"exceptions",success:function(result){
jQuery('#subApp').html(result);
document.getElementById("subLink1").className = "";
document.getElementById("subLink2").className = "selected";
document.getElementById("subLink3").className = "last_item";
if(jQuery("#fund").val() == 'all')
jQuery('#fund').val(jQuery('#fund option').filter(function() { return jQuery(this).html() == selectedFund;}).val());
jQuery('#fund option[value="all"]').prop('disabled', true);
getNavDates(0);
}});
}
The loaction redirect is not used to go to a different page, but to trigger a download.
This is when I face the problem in Chrome-
Click the download link(location redirect).
Call the AJAX function.
AJAX call hangs at showWaitMessage();
Download goes on as usual.
Note: Everything works fine on other browsers. The AJAX call on Chrome also works fine if I do that before hitting the download link.
There is a workaround for this problem – use hidden iframe for downloading instead of current window. Just define this simple function:
var $idown; // Keep it outside of the function, so it's initialized once.
function downloadURL(url) {
if ($idown) {
$idown.attr('src', url);
} else {
$idown = $('<iframe>', { id: 'idown', src: url }).hide().appendTo('body');
}
}
And replace your line:
jQuery(location).attr('href', href);
With:
downloadURL(href);
How can I check if a URL has changed in JavaScript? For example, websites like GitHub, which use AJAX, will append page information after a # symbol to create a unique URL without reloading the page. What is the best way to detect if this URL changes?
Is the onload event called again?
Is there an event handler for the URL?
Or must the URL be checked every second to detect a change?
I wanted to be able to add locationchange event listeners. After the modification below, we'll be able to do it, like this
window.addEventListener('locationchange', function () {
console.log('location changed!');
});
In contrast, window.addEventListener('hashchange',() => {}) would only fire if the part after a hashtag in a url changes, and window.addEventListener('popstate',() => {}) doesn't always work.
This modification, similar to Christian's answer, modifies the history object to add some functionality.
By default, before these modifications, there's a popstate event, but there are no events for pushstate, and replacestate.
This modifies these three functions so that all fire a custom locationchange event for you to use, and also pushstate and replacestate events if you want to use those.
These are the modifications:
(() => {
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('locationchange'));
return ret;
};
let oldReplaceState = history.replaceState;
history.replaceState = function replaceState() {
let ret = oldReplaceState.apply(this, arguments);
window.dispatchEvent(new Event('replacestate'));
window.dispatchEvent(new Event('locationchange'));
return ret;
};
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('locationchange'));
});
})();
Note, we're creating a closure, to save the old function as part of the new one, so that it gets called whenever the new one is called.
In modern browsers (IE8+, FF3.6+, Chrome), you can just listen to the hashchange event on window.
In some old browsers, you need a timer that continually checks location.hash. If you're using jQuery, there is a plugin that does exactly that.
Example
Below I undo any URL change, to keep just the scrolling:
<script type="text/javascript">
if (window.history) {
var myOldUrl = window.location.href;
window.addEventListener('hashchange', function(){
window.history.pushState({}, null, myOldUrl);
});
}
</script>
Note that above used history-API is available in Chrome, Safari, Firefox 4+, and Internet Explorer 10pp4+
window.onhashchange = function() {
//code
}
window.onpopstate = function() {
//code
}
or
window.addEventListener('hashchange', function() {
//code
});
window.addEventListener('popstate', function() {
//code
});
with jQuery
$(window).bind('hashchange', function() {
//code
});
$(window).bind('popstate', function() {
//code
});
EDIT after a bit of researching:
It somehow seems that I have been fooled by the documentation present on Mozilla docs. The popstate event (and its callback function onpopstate) are not triggered whenever the pushState() or replaceState() are called in code. Therefore the original answer does not apply in all cases.
However there is a way to circumvent this by monkey-patching the functions according to #alpha123:
var pushState = history.pushState;
history.pushState = function () {
pushState.apply(history, arguments);
fireEvents('pushState', arguments); // Some event-handling function
};
Original answer
Given that the title of this question is "How to detect URL change" the answer, when you want to know when the full path changes (and not just the hash anchor), is that you can listen for the popstate event:
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
Reference for popstate in Mozilla Docs
Currently (Jan 2017) there is support for popstate from 92% of browsers worldwide.
With jquery (and a plug-in) you can do
$(window).bind('hashchange', function() {
/* things */
});
http://benalman.com/projects/jquery-hashchange-plugin/
Otherwise yes, you would have to use setInterval and check for a change in the hash event (window.location.hash)
Update! A simple draft
function hashHandler(){
this.oldHash = window.location.hash;
this.Check;
var that = this;
var detect = function(){
if(that.oldHash!=window.location.hash){
alert("HASH CHANGED - new has" + window.location.hash);
that.oldHash = window.location.hash;
}
};
this.Check = setInterval(function(){ detect() }, 100);
}
var hashDetection = new hashHandler();
Add a hash change event listener!
window.addEventListener('hashchange', function(e){console.log('hash changed')});
Or, to listen to all URL changes:
window.addEventListener('popstate', function(e){console.log('url changed')});
This is better than something like the code below because only one thing can exist in window.onhashchange and you'll possibly be overwriting someone else's code.
// Bad code example
window.onhashchange = function() {
// Code that overwrites whatever was previously in window.onhashchange
}
this solution worked for me:
function checkURLchange(){
if(window.location.href != oldURL){
alert("url changed!");
oldURL = window.location.href;
}
}
var oldURL = window.location.href;
setInterval(checkURLchange, 1000);
None of these seem to work when a link is clicked that which redirects you to a different page on the same domain. Hence, I made my own solution:
let pathname = location.pathname;
window.addEventListener("click", function() {
if (location.pathname != pathname) {
pathname = location.pathname;
// code
}
});
Edit: You can also check for the popstate event (if a user goes back a page)
window.addEventListener("popstate", function() {
// code
});
Best wishes,
Calculus
If none of the window events are working for you (as they aren't in my case), you can also use a MutationObserver that looks at the root element (non-recursively).
// capture the location at page load
let currentLocation = document.location.href;
const observer = new MutationObserver((mutationList) => {
if (currentLocation !== document.location.href) {
// location changed!
currentLocation = document.location.href;
// (do your event logic here)
}
});
observer.observe(
document.getElementById('root'),
{
childList: true,
// important for performance
subtree: false
});
This may not always be feasible, but typically, if the URL changes, the root element's contents change as well.
I have not profiled, but theoretically this has less overhead than a timer because the Observer pattern is typically implemented so that it just loops through the subscriptions when a change occurs. We only added one subscription here. The timer on the other hand would have to check very frequently in order to ensure that the event was triggered immediately after URL change.
Also, this has a good chance of being more reliable than a timer since it eliminates timing issues.
Although an old question, the Location-bar project is very useful.
var LocationBar = require("location-bar");
var locationBar = new LocationBar();
// listen to all changes to the location bar
locationBar.onChange(function (path) {
console.log("the current url is", path);
});
// listen to a specific change to location bar
// e.g. Backbone builds on top of this method to implement
// it's simple parametrized Backbone.Router
locationBar.route(/some\-regex/, function () {
// only called when the current url matches the regex
});
locationBar.start({
pushState: true
});
// update the address bar and add a new entry in browsers history
locationBar.update("/some/url?param=123");
// update the address bar but don't add the entry in history
locationBar.update("/some/url", {replace: true});
// update the address bar and call the `change` callback
locationBar.update("/some/url", {trigger: true});
To listen to url changes, see below:
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
Use this style if you intend to stop/remove listener after some certain condition.
window.addEventListener('popstate', function(e) {
console.log('url changed')
});
The answer below comes from here(with old javascript syntax(no arrow function, support IE 10+)):
https://stackoverflow.com/a/52809105/9168962
(function() {
if (typeof window.CustomEvent === "function") return false; // If not IE
function CustomEvent(event, params) {
params = params || {bubbles: false, cancelable: false, detail: null};
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
window.CustomEvent = CustomEvent;
})();
(function() {
history.pushState = function (f) {
return function pushState() {
var ret = f.apply(this, arguments);
window.dispatchEvent(new CustomEvent("pushState"));
window.dispatchEvent(new CustomEvent("locationchange"));
return ret;
};
}(history.pushState);
history.replaceState = function (f) {
return function replaceState() {
var ret = f.apply(this, arguments);
window.dispatchEvent(new CustomEvent("replaceState"));
window.dispatchEvent(new CustomEvent("locationchange"));
return ret;
};
}(history.replaceState);
window.addEventListener("popstate", function() {
window.dispatchEvent(new CustomEvent("locationchange"));
});
})();
While doing a little chrome extension, I faced the same problem with an additionnal problem : Sometimes, the page change but not the URL.
For instance, just go to the Facebook Homepage, and click on the 'Home' button. You will reload the page but the URL won't change (one-page app style).
99% of the time, we are developping websites so we can get those events from Frameworks like Angular, React, Vue etc..
BUT, in my case of a Chrome extension (in Vanilla JS), I had to listen to an event that will trigger for each "page change", which can generally be caught by URL changed, but sometimes it doesn't.
My homemade solution was the following :
listen(window.history.length);
var oldLength = -1;
function listen(currentLength) {
if (currentLength != oldLength) {
// Do your stuff here
}
oldLength = window.history.length;
setTimeout(function () {
listen(window.history.length);
}, 1000);
}
So basically the leoneckert solution, applied to window history, which will change when a page changes in a single page app.
Not rocket science, but cleanest solution I found, considering we are only checking an integer equality here, and not bigger objects or the whole DOM.
Found a working answer in a separate thread:
There's no one event that will always work, and monkey patching the pushState event is pretty hit or miss for most major SPAs.
So smart polling is what's worked best for me. You can add as many event types as you like, but these seem to be doing a really good job for me.
Written for TS, but easily modifiable:
const locationChangeEventType = "MY_APP-location-change";
// called on creation and every url change
export function observeUrlChanges(cb: (loc: Location) => any) {
assertLocationChangeObserver();
window.addEventListener(locationChangeEventType, () => cb(window.location));
cb(window.location);
}
function assertLocationChangeObserver() {
const state = window as any as { MY_APP_locationWatchSetup: any };
if (state.MY_APP_locationWatchSetup) { return; }
state.MY_APP_locationWatchSetup = true;
let lastHref = location.href;
["popstate", "click", "keydown", "keyup", "touchstart", "touchend"].forEach((eventType) => {
window.addEventListener(eventType, () => {
requestAnimationFrame(() => {
const currentHref = location.href;
if (currentHref !== lastHref) {
lastHref = currentHref;
window.dispatchEvent(new Event(locationChangeEventType));
}
})
})
});
}
Usage
observeUrlChanges((loc) => {
console.log(loc.href)
})
I created this event that is very similar to the hashchange event
// onurlchange-event.js v1.0.1
(() => {
const hasNativeEvent = Object.keys(window).includes('onurlchange')
if (!hasNativeEvent) {
let oldURL = location.href
setInterval(() => {
const newURL = location.href
if (oldURL === newURL) {
return
}
const urlChangeEvent = new CustomEvent('urlchange', {
detail: {
oldURL,
newURL
}
})
oldURL = newURL
dispatchEvent(urlChangeEvent)
}, 25)
addEventListener('urlchange', event => {
if (typeof(onurlchange) === 'function') {
onurlchange(event)
}
})
}
})()
Example of use:
window.onurlchange = event => {
console.log(event)
console.log(event.detail.oldURL)
console.log(event.detail.newURL)
}
addEventListener('urlchange', event => {
console.log(event)
console.log(event.detail.oldURL)
console.log(event.detail.newURL)
})
for Chrome 102+ (2022-05-24)
navigation.addEventListener("navigate", e => {
console.log(`navigate ->`,e.destination.url)
});
API references WICG/navigation-api
Look at the jQuery unload function. It handles all the things.
https://api.jquery.com/unload/
The unload event is sent to the window element when the user navigates away from the page. This could mean one of many things. The user could have clicked on a link to leave the page, or typed in a new URL in the address bar. The forward and back buttons will trigger the event. Closing the browser window will cause the event to be triggered. Even a page reload will first create an unload event.
$(window).unload(
function(event) {
alert("navigating");
}
);
window.addEventListener("beforeunload", function (e) {
// do something
}, false);
You are starting a new setInterval at each call, without cancelling the previous one - probably you only meant to have a setTimeout
Enjoy!
var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
if (location.href !== previousUrl) {
previousUrl = location.href;
console.log(`URL changed to ${location.href}`);
}
});
Another simple way you can do this is by adding a click event, through a class name to the anchor tags on the page to detect when it has been clicked, then you can now use the window.location.href to get the url data which you can use to run your ajax request to the server. Simple and Easy.