JavaScript img.onerror changing source - race condition - javascript

The following code will register an onerror function for an image element
(function() {
var imgElements = document.getElementsByTagName('img');
for(i = 0; i < imgElements.length; i++) {
(function() {
imgElements[i].onerror = function() {
this.src = base_url() + 'assets/images/placeholder.jpg';
}
})();
}
})();
This code works only sometimes. (I'm using chrome); If i hold down F5 or refresh the page very fast, it seems like the onerror function does not get executed.
For example: If i load the page, then wait for a few seconds, and refresh again the src will change, but not all the time.
I believe this is some type of caching issue with the browser?
More specifically, If I press the refresh icon on chrome, everything will work, even on abrupt refreshes.
But, if I highlight the URL and press return, the code does not end up changing the src to my placeholder image.
Can you give me any insight into why this is happening, and suggest a way to circumvent this?

You cannot control the timing of things to make this work this way reliably the way you are trying to do it. Here's the order things happen:
The browser fetches the HTML for the page
The browser starts parsing the HTML
As it finds an <img> tag, it fires off a request to load the src URL.
The browser discovers that the src URL cannot be loaded and thus fires the onerror handler.
Your javascript runs and installs onerror handlers.
Now, the order of steps 4 and 5 is purely timing related and is not under your control. Some conditions may cause it to occur in this order and some conditions with 4 and 5 reversed. And, keep in mind that once your page has been loaded once, parts of it may be cached and the timing sequence can be different in subsequent loads. Some browsers may even cache the fact that a URL is currently unreachable and may avoid trying it again if it is immediately requests again (thus generating an immediate onerror response).
In addition, some browsers do not behave entirely properly when the .src property is set to a new value. As of a couple years ago, I know that some browsers did not reliably fire the onload event when the .src propertyw was changed and a new image was loaded. The same could be true for onerror handlers. My experience is that they do work reliably for first time image loads, but I'm not sure they work reliably when setting a different .src property.
To have an onerror handler that will not be missed, you must have it in place before the .src property is set or parsed. Here are two ways to do that.
Put the onerror handler in the HTML itself so it's part of the <img> tag.
Don't have fully formed <img> tags in your HTML (e.g. no .src or not in the HTML at all) and then with your javascript, you can add the onerror handler and THEN create the images or set the .src appropriately.
The key is that the onerror handler MUST be installed before the .src property is set in any way.
Solution #1 looks like this:
<img src="xxx.jpg" onerror="setErrorImage(this)">
// this function must be in the global scope
function setErrorImage(img) {
// clear error handler so no chance of looping
img.onerror = function() {};
img.src = base_url() + 'assets/images/placeholder.jpg';
}
One way to implement solution #2 looks like this:
<img data-src="xxx.jpg">
(function() {
function setErrorImage() {
this.removeEventListener("error", setErrorImage);
this.src = base_url() + 'assets/images/placeholder.jpg';
}
var imgElements = document.getElementsByTagName('img');
var img, url;
for (var i = 0; i < imgElements.length; i++) {
img = imgElements[i];
// now that onerror handler is in place, set the .src property
url = img.getAttribute("data-src")
if (!img.src && url) {
img.addEventListener("error", setErrorImage);
img.src = url;
}
}
})();
You could also have no <img> tags in the HTML at all and create all of them dynamically via javascript, making sure that the onerror handlers were installed before the .src property was set, but this tends to make the design process a bit more complicated. The above solution for option #2 is generally preferred over creating the image objects entirely in code.

Related

Jquery click handler from setTimeout

I have <a> element which clicked runs handler like:
function onCummReportClick(e) {
if ($(e.currentTarget).attr('href').indexOf('csv') !== -1) {
{
return true;
}
//Here some files loaded asynchronousely
$.ajax().success(...).always(...);
var reports = [];
var downloadedCount = 0;
var reportsCount = 9;
var oneDownloaded = function() {
if (downloadedCount === reportsCount) {
//Prepare cummulative reports CSV
$(e.currentTarget).attr('href', 'data:application/csv;charset=utf-8,'+ encodeURI(reports.join('\n\n')))
//HERE!
$(e.currentTarget).trigger('click');
} else {
setTimeout(oneDownloaded, 1000);
}
};
setTimeout(oneDownloaded, 1000);
}
I.e. it download reports, join its content to one CSV and set it as base64-encoded to original <a>'s href. After that I want it downloaded automatically without user have to click again.
And return true does not generates "normal" click flow, looks like if just skipped. i mean that I running into this handler second time, but download did not started.
If I set up href to generated value statically and in handler just returning true if works as expected - file automatically downloaded.
I know that I could download reports synchronously and make entire method sync, but I dislike this approach.
UPD: I created jsfiddle which demonstrates what I trying to do: https://jsfiddle.net/u1rs17w1/
I do not know why code from JSFiddle does not works, as I understand all assumption about "lost context" is wrong (at least half wrong) because original hyperlink is updated correctly (I mean href attribute uptdated).
But I found another solution. Instead of clicking on original link I do following:
var link = document.createElement('a');
link.download = _messages.CUMMULATIVE_REPORT_NAME;
link.href='data:application/csv;charset=utf-8,'+ encodeURI(csvFile.join('\n\n'));
//These 2 lines are required for Firefox
link.style = "display: none";
document.body.appendChild(link);
link.click();
And it is works even from setTimeout handler. I suspect that clicks by jquery-style, i.e. with return true (I cannot call it somehow clearly) works only from main thread. Each time I works with setTimeout execution continued in new thread. But creating element in new thread and clicking on it works like a charm!
Happy coding!
UPD: This solution does not works in Firefox. Nothing works in Firefox unless synchronous AJAX (ffffffuuuuuuuu!) and return true.
UPD1: Added solution for Firefox.

does unhandled postMessage leak memory?

I have a page that may work as a standalone webpage or within a special "watchdog frame" to which it would be sending messages periodically using window.top.postMessage('message', '*');
The problem is if the page is loaded as standalone webpage, there will be nothing to capture these messages. Users can be expected to hold that page open for hours or even days at a time.
Do these uncaught messages just vanish without a trace, or do they get queued or something like that, leaking the memory?
postMessage initiates a MessageEvent on the target, which is not more special than a regular onload event.
The event itself does not cause any memory leaks. You can create memory leaks by introducing unwanted closures, but that also applies to regular functions:
window.addEventListener('message', messageHandler, false);
function messageHandler(event) {
var img = new Image();
img.onload = function() {
document.body.appendChild(img);
};
img.src = event.data.some.property.deep.ly.nested.obj.bad.coding.style.src;
}
What's the problem? The image's onload handler inserts the picture in the document when it's loaded. However, because of the closure, the event object cannot be freed, and the big event.data object will still occupy memory.
Another way to introduce a memory leak (same-origin only) is to save the value of event.source (preventing the frame's view from being GC'd when the frame is removed):
var stupidity = [];
window.onmessage = function(event) {
stupidity.push(event.source);
};

Loading message

Searching for a js script, which will show some message (something like "Loading, please wait") until the page loads all images.
Important - it mustn't use any js framework (jquery, mootools, etc), must be an ordinary js script.
Message must disappear when the page is loaded.
Yeah an old-school question!
This goes back to those days when we used to preload images...
Anyway, here's some code. The magic is the "complete" property on the document.images collection (Image objects).
// setup a timer, adjust the 200 to some other milliseconds if desired
var _timer = setInterval("imgloaded()",200);
function imgloaded() {
// assume they're all loaded
var loaded = true;
// test all images for "complete" property
for(var i = 0, len = document.images.length; i < len; i++) {
if(!document.images[i].complete) { loaded = false; break; }
}
// if loaded is still true, change the HTML
if(loaded) {
document.getElementById("msg").innerHTML = "Done.";
// clear the timer
clearInterval(_timer);
}
};
Of course, this assumes you have some DIV thrown in somewhere:
<div id="msg">Loading...</div>
Just add a static <div> to the page, informing user that the page is loading. Then add window.onload handler and remove the div.
BTW, what’s the reason of this? Don’t users already have page load indicators in their browsers?
You should do async ajax requests for the images and add a call back when it's finished.
Here's some code to illustrate it:
var R = new XMLHttpRequest();
R.onreadystatechange = function() {
if (R.readyState == 4) {
// Do something with R.responseXML/Text ...
stopWaiting();
}
};
Theoretically you could have an onload event on every image object that runs a function that checks if all images is loaded. This way you don´t need a setTimeOut(). This would however fail if an image didn´t load so you would have to take onerror into account also.

The definitive best way to preload images using JavaScript/jQuery?

I'm fully aware that this question has been asked and answered everywhere, both on SO and off. However, every time there seems to be a different answer, e.g. this, this and that.
I don't care whether it's using jQuery or not - what's important is that it works, and is cross-browser.]
So, what is the best way to preload images?
Unfortunately, that depends on your purpose.
If you plan to use the images for purposes of style, your best bet is to use sprites.
http://www.alistapart.com/articles/sprites2
However, if you plan to use the images in <img> tags, then you'll want to pre-load them with
function preload(sources)
{
var images = [];
for (i = 0, length = sources.length; i < length; ++i) {
images[i] = new Image();
images[i].src = sources[i];
}
}
(modified source taken from What is the best way to preload multiple images in JavaScript?)
using new Image() does not involve the expense of using DOM methods but a new request for the image specified will be added to the queue. As the image is, at this point, not actually added to the page, there is no re-rendering involved. I would recommend, however, adding this to the end of your page (as all of your scripts should be, when possible) to prevent it from holding up more critical elements.
Edit: Edited to reflect comment quite correctly pointing out that separate Image objects are required to work properly. Thanks, and my bad for not checking it more closely.
Edit2: edited to make the reusability more obvious
Edit 3 (3 years later):
Due to changes in how browsers handle non-visible images (display:none or, as in this answer, never appended to the document) a new approach to pre-loading is preferred.
You can use an Ajax request to force early retrieval of images. Using jQuery, for example:
jQuery.get(source);
Or in the context of our previous example, you could do:
function preload(sources)
{
jQuery.each(sources, function(i,source) { jQuery.get(source); });
}
Note that this doesn't apply to the case of sprites which are fine as-is. This is just for things like photo galleries or sliders/carousels with images where the images aren't loading because they are not visible initially.
Also note that this method does not work for IE (ajax is normally not used to retrieve image data).
Spriting
As others have mentioned, spriting works quite well for a variety of reasons, however, it's not as good as its made out to be.
On the upside, you end up making only one HTTP request for your images. YMMV though.
On the down side you are loading everything in one HTTP request. Since most current browsers are limited to 2 concurrent connections the image request can block other requests. Hence YMMV and something like your menu background might not render for a bit.
Multiple images share the same color palette so there is some saving but this is not always the case and even so it's negligible.
Compression is improved because there is more shared data between images.
Dealing with irregular shapes is tricky though. Combining all new images into the new one is another annoyance.
Low jack approach using <img> tags
If you are looking for the most definitive solution then you should go with the low-jack approach which I still prefer. Create <img> links to the images at the end of your document and set the width and height to 1x1 pixel and additionally put them in a hidden div. If they are at the end of the page, they will be loaded after other content.
As of January 2013 none of the methods described here worked for me, so here's what did instead, tested and working with Chrome 25 and Firefox 18. Uses jQuery and this plugin to work around the load event quirks:
function preload(sources, callback) {
if(sources.length) {
var preloaderDiv = $('<div style="display: none;"></div>').prependTo(document.body);
$.each(sources, function(i,source) {
$("<img/>").attr("src", source).appendTo(preloaderDiv);
if(i == (sources.length-1)) {
$(preloaderDiv).imagesLoaded(function() {
$(this).remove();
if(callback) callback();
});
}
});
} else {
if(callback) callback();
}
}
Usage:
preload(['/img/a.png', '/img/b.png', '/img/c.png'], function() {
console.log("done");
});
Note that you'll get mixed results if the cache is disabled, which it is by default on Chrome when the developer tools are open, so keep that in mind.
In my opinion, using Multipart XMLHttpRequest introduced by some libraries will be a preferred solution in the following years. However IE < v8, still don't support data:uri (even IE8 has limited support, allowing up to 32kb). Here is an implementation of parallel image preloading - http://code.google.com/p/core-framework/wiki/ImagePreloading , it's bundled in framework but still worth taking a look.
This was from a long time ago so I dont know how many people are still interested in preloading an image.
My solution was even more simple.
I just used CSS.
#hidden_preload {
height: 1px;
left: -20000px;
position: absolute;
top: -20000px;
width: 1px;
}
Here goes my simple solution with a fade in on the image after it is loaded.
function preloadImage(_imgUrl, _container){
var image = new Image();
image.src = _imgUrl;
image.onload = function(){
$(_container).fadeTo(500, 1);
};
}
For my use case I had a carousel with full screen images that I wanted to preload. However since the images display in order, and could take a few seconds each to load, it's important that I load them in order, sequentially.
For this I used the async library's waterfall() method (https://github.com/caolan/async#waterfall)
// Preload all images in the carousel in order.
image_preload_array = [];
$('div.carousel-image').each(function(){
var url = $(this).data('image-url');
image_preload_array.push(function(callback) {
var $img = $('<img/>')
$img.load(function() {
callback(null);
})[0].src = url;
});
});
async.waterfall(image_preload_array);
This works by creating an array of functions, each function is passed the parameter callback() which it needs to execute in order to call the next function in the array. The first parameter of callback() is an error message, which will exit the sequence if a non-null value is provided, so we pass null each time.
See this:
http://www.mattfarina.com/2007/02/01/preloading_images_with_jquery
Related question on SO:
jquery hidden preload

Detect failure to load contents of an iframe

I can detect when the content of an iframe has loaded using the load event. Unfortunately, for my purposes, there are two problems with this:
If there is an error loading the page (404/500, etc), the load event is never fired.
If some images or other dependencies failed to load, the load event is fired as usual.
Is there some way I can reliably determine if either of the above errors occurred?
I'm writing a semi-web semi-desktop application based on Mozilla/XULRunner, so solutions that only work in Mozilla are welcome.
If you have control over the iframe page (and the pages are on the same domain name), a strategy could be as follows:
In the parent document, initialize a variable var iFrameLoaded = false;
When the iframe document is loaded, set this variable in the parent to true calling from the iframe document a parent's function (setIFrameLoaded(); for example).
check the iFrameLoaded flag using the timer object (set the timer to your preferred timeout limit) - if the flag is still false you can tell that the iframe was not regularly loaded.
I hope this helps.
This is a very late answer, but I will leave it to someone who needs it.
Task: load iframe cross-origin content, emit onLoaded on success and onError on load error.
This is the most cross browsers origin independent solution I could develop. But first of all I will briefly tell about other approaches I had and why they are bad.
1. iframe That was a little shock for me, that iframe only has onload event and it is called on load and on error, no way to know it is error or not.
2. performance.getEntriesByType('resource'). This method returns loaded resources. Sounds like what we need. But what a shame, firefox always adds Resource in resources array no matter it is loaded or failed. No way to know by Resource instance was it success. As usual. By the way, this method does not work in ios<11.
3. script I tried to load html using <script> tag. Emits onload and onerror correctly, sadly, only in Chrome.
And when I was ready to give up, my elder collegue told me about html4 tag <object>. It is like <iframe> tag except it has fallbacks when content is not loaded. That sounds like what we are need! Sadly it is not as easy as it sounds.
CODE SECTION
var obj = document.createElement('object');
// we need to specify a callback (i will mention why later)
obj.innerHTML = '<div style="height:5px"><div/>'; // fallback
obj.style.display = 'block'; // so height=5px will work
obj.style.visibility = 'hidden'; // to hide before loaded
obj.data = src;
After this we can set some attributes to <object> like we'd wanted to do with iframe. The only difference, we should use <params>, not attributes, but their names and values are identical.
for (var prop in params) {
if (params.hasOwnProperty(prop)) {
var param = document.createElement('param');
param.name = prop;
param.value = params[prop];
obj.appendChild(param);
}
}
Now, the hard part. Like many same-like elements, <object> doesn't have specs for callbacks, so each browser behaves differently.
Chrome. On error and on load emits load event.
Firefox. Emits load and error correctly.
Safari. Emits nothing....
Seems like no different from iframe, getEntriesByType, script....
But, we have native browser fallback! So, because we set fallback (innerHtml) directly, we can tell if <object> is loaded or not
function isReallyLoaded(obj) {
return obj.offsetHeight !== 5; // fallback height
}
/**
* Chrome calls always, Firefox on load
*/
obj.onload = function() {
isReallyLoaded(obj) ? onLoaded() : onError();
};
/**
* Firefox on error
*/
obj.onerror = function() {
onError();
};
But what to do with Safari? Good old setTimeout.
var interval = function() {
if (isLoaded) { // some flag
return;
}
if (hasResult(obj)) {
if (isReallyLoaded(obj)) {
onLoaded();
} else {
onError();
}
}
setTimeout(interval, 100);
};
function hasResult(obj) {
return obj.offsetHeight > 0;
}
Yeah.... not so fast. The thing is, <object> when fails has unmentioned in specs behaviour:
Trying to load (size=0)
Fails (size = any) really
Fallback (size = as in innnerHtml)
So, code needs a little enhancement
var interval = function() {
if (isLoaded) { // some flag
return;
}
if (hasResult(obj)) {
if (isReallyLoaded(obj)) {
interval.count++;
// needs less then 400ms to fallback
interval.count > 4 && onLoadedResult(obj, onLoaded);
} else {
onErrorResult(obj, onError);
}
}
setTimeout(interval, 100);
};
interval.count = 0;
setTimeout(interval, 100);
Well, and to start loading
document.body.appendChild(obj);
That is all. I tried to explain code in every detail, so it may look not so foolish.
P.S. WebDev sucks
I had this problem recently and had to resort to setting up a Javascript Polling action on the Parent Page (that contains the IFRAME tag). This JavaScript function checks the IFRAME's contents for explicit elements that should only exist in a GOOD response. This assumes of course that you don't have to deal with violating the "same origin policy."
Instead of checking for all possible errors which might be generated from the many different network resources.. I simply checked for the one constant positive Element(s) that I know should be in a good response.
After a pre-determined time and/or # of failed attempts to detect the expected Element(s), the JavaScript modifies the IFRAME's SRC attribute (to request from my Servlet) a User Friendly Error Page as opposed to displaying the typical HTTP ERROR message. The JavaScript could also just as easily modify the SRC attribute to make an entirely different request.
function checkForContents(){
var contents=document.getElementById('myiframe').contentWindow.document
if(contents){
alert('found contents of myiframe:' + contents);
if(contents.documentElement){
if(contents.documentElement.innerHTML){
alert("Found contents: " +contents.documentElement.innerHTML);
if(contents.documentElement.innerHTML.indexOf("FIND_ME") > -1){
openMediumWindow("woot.html", "mypopup");
}
}
}
}
}
I think that the pageshow event is fired for error pages. Or if you're doing this from chrome, then your check your progress listener's request to see if it's an HTTP channel in which case you can retrieve the status code.
As for page dependencies, I think you can only do this from chrome by adding a capturing onerror event listener, and even then it will only find errors in elements, not CSS backgrounds or other images.
Doesn't answer your question exactly, but my search for an answer brought me here, so I'm posting just in case anyone else had a similar query to me.
It doesn't quite use a load event, but it can detect whether a website is accessible and callable (if it is, then the iFrame, in theory, should load).
At first, I thought to do an AJAX call like everyone else, except that it didn't work for me initially, as I had used jQuery. It works perfectly if you do a XMLHttpRequest:
var url = http://url_to_test.com/
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status != 200) {
console.log("iframe failed to load");
}
};
xhttp.open("GET", url, true);
xhttp.send();
Edit:
So this method works ok, except that it has a lot of false negatives (picks up a lot of stuff that would display in an iframe) due to cross-origin malarky. The way that I got around this was to do a CURL/Web request on a server, and then check the response headers for a) if the website exists, and b) if the headers had set x-frame-options.
This isn't a problem if you run your own webserver, as you can make your own api call for it.
My implementation in node.js:
app.get('/iframetest',function(req,res){ //Call using /iframetest?url=url - needs to be stripped of http:// or https://
var url = req.query.url;
var request = require('https').request({host: url}, function(response){ //This does an https request - require('http') if you want to do a http request
var headers = response.headers;
if (typeof headers["x-frame-options"] != 'undefined') {
res.send(false); //Headers don't allow iframe
} else {
res.send(true); //Headers don't disallow iframe
}
});
request.on('error',function(e){
res.send(false); //website unavailable
});
request.end();
});
Have a id for the top most (body) element in the page that is being loaded in your iframe.
on the Load handler of your iframe, check to see if getElementById() returns a non null value.
If it is, iframe has loaded successfully. else it has failed.
in that case, put frame.src="about:blank". Make sure to remove the loadhandler before doing that.
If the iframe is loaded on the same origin as the parent page, then you can do this:
iframeEl.addEventListener('load', function() {
// NOTE: contentDocument is null if a connection error occurs or if
// X-Frame-Options is not SAMESITE (which could happen with
// 4xx or 5xx error pages if the corresponding error handlers
// do not specify SAMESITE). If error handlers do not specify
// SAMESITE, then networkErrorOccurred will incorrectly be set
// to true.
const networkErrorOccurred = !iframeEl.contentDocument;
const serverErrorOccurred = (
!networkErrorOccurred &&
!iframeEl.contentDocument.querySelector('#well-known-element')
);
if (networkErrorOccurred || serverErrorOccurred) {
let errorMessage;
if (networkErrorOccurred) {
errorMessage = 'Error: Network error';
} else if (serverErrorOccurred) {
errorMessage = 'Error: Server error';
} else {
// Assert that the above code is correct.
throw new Error('networkErrorOccurred and serverErrorOccurred are both false');
}
alert(errorMessage);
}
});

Categories

Resources