I have a web page which is chock full of javascript, and a few references to resources like images for the javascript to work with. I use a websocket to communicate with the server; the javascript parses the socket's data and does things with the page presentation accordingly. It all works fine, except when it doesn't.
The problem appears to be that that page contains images which I want to display parts of, under javascript control. No matter how I play with defer, there are apparently situations in which the images don't seem to be fully downloaded before the javascript tries to use them. The result is images are missing when the page is rendered, some small percentage of the time.
I'm not very used to languages and protocols where you don't have strict control over what happens when, so the server and browser shipping stuff and executing stuff in an uncontrolled and asynch order annoys me. So I'd like to stop depending on apparently unreliable tricks like defer. What I'd like to do is just download the whole page, and then open my websocket and send my images and other resources down through it. When that process is complete, I'll know it's safe to accept other commands from the websocket and get on with doing what the page does. In other words I want to subvert the browsers asynch handling of resources, and handle it all serially under javascript control.
Pouring an image file from the server down a socket is easy and I have no trouble coming up with protocols to do it. Capturing the data as byte arrays, also easy.
But how do I get them interpreted as images?
I know there are downsides to this approach. I won't get browser caching of my images and the initial page won't load as quickly. I'm ok with that. I'm just tired of 95% working solutions and having to wonder if what I did works in every browser imaginable. (Working on everything from IE 8 to next year's Chrome is a requirement for me.)
Is this approach viable? Are there better ways to get strict, portable control of resource loading?
You still haven't been very specific about what resources you are waiting for other than images, but if they are all images, then you can use this loadMonitor object to monitor when N images are done loading:
function loadMonitor(/* img1, img2, img3 */) {
var cntr = 0, doneFn, self = this;
function checkDone() {
if (cntr === 0 && doneFn) {
// clear out doneFn so nothing that is done in the doneFn callback
// accidentally cause the callback to get called again
var f = doneFn;
doneFn = null;
f.call(self);
}
}
function handleEvents(obj, eventList) {
var events = eventList.split(" "), i;
function handler() {
--cntr;
for (i = 0; i < events.length; i++) {
obj.removeEventListener(events[i], handler);
}
checkDone();
}
for (i = 0; i < events.length; i++) {
obj.addEventListener(events[i], handler);
}
}
this.add = function(/* img1, img2, img3 */) {
if (doneFn) {
throw new Error("Can't call loadMonitor.add() after calling loadMonitor.start(fn)");
}
var img;
for (var i = 0; i < arguments.length; i++) {
img = arguments[i];
if (!img.src || !img.complete) {
++cntr;
handleEvents(img, "load error abort");
}
}
};
this.start = function(fn) {
if (!fn) {
throw new Error("must pass completion function as loadMonitor.start(fn)");
}
doneFn = fn;
checkDone();
};
// process constructor arguments
this.add.apply(this, arguments);
}
// example usage code
var cardsImage = new Image();
cardsImage.src = ...
var playerImage = new Image();
playerImage.src = ...
var tableImage = new Image();
var watcher = new loadMonitor(cardsImage, playerImage, tableImage);
// .start() tells the monitor that all images are now in the monitor
// and passes it our callback so it can now tell us when things are done
watcher.start(function() {
// put code here that wants to run when all the images are loaded
});
// the .src value can be set before or after the image has been
// added to the loadMonitor
tableImage.src = ...
Note, you must make sure that all images you put in the loadMonitor do get a .src assigned or the loadMonitor will never call its callback because that image will never finish.
Working demo: http://jsfiddle.net/jfriend00/g9x74d2j/
Related
I just want to disable certain buttons and show a loading spinner until images are loaded.
This is a similar question- How to detect when an image has finished rendering in the browser (i.e. painted)?
I am new to javascript and am confused why there is not a simple way to determine this! Something similar to this-
document.addEventListener('readystatechange', event => {
if (event.target.readyState === 'complete') {
alert('complete');
}
});
Unfortunately after much searching it seems I will need to use a callback or a promise. I figured it would be a good time to learn about promises/async/await. I am having difficulty attempting to rewrite my functions using promises.
$('#container').on('click', '#imgSearch', function (e) {
$(this).html(`
<i class="fa fa-spinner fa-spin"></i>Loading`);
//various if statements here to check radio buttons
showImgs(search, sort, phpController, limit);
}
function showImgs(search, sort, phpController, limit) {
imgArray = [];
var a1 = $.post(phpController, {search: search, sort: sort, limit: limit}, {dataType: 'json'});
$.when(a1).done(function (response) {
if (response['error']) {
alert(response.error.img);
} else {
var imgs = response;
if (Array.isArray(imgs)) {
for (let i = 0; i < imgs.length; i++) {
//I use setTimeout here to render one image per second
setTimeout(function () {
offset++;
saveImages(imgs[i]);
//calling alertFinished on last img is my temporary solution to removing spinner after render
if (i === imgs.length - 1) {
alertFinished();
}
}, 1000 * i);
}
} else {
saveImages(imgs);
}
}
});
}
;
I use the saveImages function to empty and push to another array as well as other purposes not shown-
function saveImages(img) {
imgArray = [];
imgArray.push(img);
displayImages(imgArray);
}
;
displayImages renders the image while adding classes etc.-
function displayImages(imgs) {
var counter = 0;
imgs.forEach(img => {
var html = '';
html += `<div class="" id='${img.id}'><img src=${img.image}></div>`;
$(html).hide().appendTo(imgSection).fadeIn(1000);
});
}
;
AlertFinished removes the loading spinner from search button.
function alertFinished() {
$('#imgSearch').text('Search');
}
Will someone please explain how I can use promises/async/await in this context? It's unfortunate there isn't a way to use an event listener similar to readystatechange because the alternative is to refactor every function that renders new elements in the DOM in order to disable buttons, show spinner etc. Showing a spinner/loading msg is such a widespread feature I am surprised I am having difficulty implementing it appropriately.
So first off, as you can see from the question you linked to (and comments therein), there isn't a great way to tell when an image is actually painted. So I'll focus on a way you can call some function after images are loaded.
For starters, if you have a bunch of images in your page right as it first loads, and you just want to wait for them to load before doing something, you could try the super simple route which would be the window load event.
window.addEventListener('load', (event) => {
// do stuff
});
But I get the impression you have a situation where you're adding images dynamically and want to do something after they're loaded, so that won't work. Still, you're overcomplicating things, I think. A quick look at your function shows you're calling saveImages and displayImages inside a loop even though they appear to be things you want to do only once after you're done adding images.
Assuming that at some point in your whole process you find yourself with a bunch of images that have been added to your DOM, at least some of which are still in the middle of loading, what you need to do is check for the last image to be loaded and then remove your loading spinner afterwards.
The trick here is figuring out which image is last to load. It won't necessarily be the last one you added because images added earlier on could be larger.
One approach you can use to skip the whole promise/async confusion all together would be to use a recursive function that every so often checks whether all images are loaded and if not waits a bit and then checks again. If you make the wait before the function calls itself again relatively short, this will likely work just fine for you.
Something along these lines:
const registeredImages = [];
// add your images
for (let i = 0, l = someImages.length; i < l; i += 1) {
doSomething(); // add someImages[i] to DOM where you want it
someImages[i].setAttribute('data-id', `image-${i}`); // or whatever way to keep track of it
someImages[i].addEventListener('load', register);
}
// remove spinner once they're loaded
tryToRemoveSpinner();
function register(event) {
images.push(event.target.getAttribute('data-id');
}
function tryToRemoveSpinner {
if (registeredImages.length === someImages.length) {
removeSpinner(); // or whatever
} else {
setTimeout(() => { tryToRemoveSpinner() }, 100);
}
}
An enhancement you could add here would be to put some kind of counter on tryToRemoveSpinner so if some image fails to load for whatever reason it eventually just bails out or runs removeSpinner() anyway or whatever you want to do in the event of an error, just in case.
Related reading: How to create a JavaScript callback for knowing when an image is loaded
My requirement is that, to showcase images available in the directory to the browser view. Using javascript. For that, i have named all images with a same prefix and changed the number to the each image at the end.as shown below.
Folder
gal-img-1.jpg
gal-img-2.jpg
gal-img-3.jpg
gal-img-4.jpg
...
now the code i written something like below.
for(j=1;j<=10;j++){
var objImg = new Image();
var imgSrc = '/assets/images/work/gal-img-'+j+'.jpg'
objImg.src = imgSrc;
objImg.onerror = function (){
j = 11;
console.log("done")
}
}
Now, when i check console, i can see the loop is not stopping when there is no image as 'gal-img-5.jpg'. i see 404 not found error for 5,6,7,8,9,10. after these errors i see "done"(console.log).
Here i want is when there is no image in the directory the for loop needs to stop.
Can someone help me on this.
You can use a bit of recursion using Jaromanda X's mentioned onload method.
function loadImages(i) {
objImg = new Image();
objImg.src = `/assets/images/work/gal-img-${i}.jpg`;
objImg.onload = function () {
if (i < 11) loadImages(++i); // Always have an exit condition with recursion
}
}
// You need to call loadImages(0)
The above would show an error message for the file not found and will not try to load more images.
I've scouted many forums and blogs and questions and sites and whatnot but cannot seem to find a solution that works for me - I am trying to load images using pure javascript without halting the rest of the page to load, and without relying on third party libraries.
On the site I work on, there may be between 0 - 30 images that may load, of different resolutions, and as you may imagine, might slow down performance to a halt on slower connections (which is what I am trying to prevent now - I want the user to see info on the page and worry less about images hooting up the performance on it)
on my latest attempt:
(function () {
// jquery is unavailable here. using javascript counterpart.
var carouselDivs = document.querySelectorAll('#caruselImagesDivs div[data-url]');
var carouselIndicators = document.querySelector('.carousel-indicators');
var carouselInner = document.querySelector('.carousel-inner');
for (var i = 0; i < carouselDivs.length; i++) {
var liIndicator = document.createElement('LI');
liIndicator.dataset.target = "#property_image_gallery";
liIndicator.dataset.slideTo = i + 1;
var divItem = document.createElement('DIV');
divItem.className = "item";
var image = document.createElement('IMG');
image.dataset.src = carouselDivs[i].dataset.url;
image.classname = 'img-responsive center-block';
// for some reason I thought this might work, but it hasn't.
image.onload = function () {
image.src = image.dataset.src;
image.onload = function () { };
}
image.src = '/Images/blankbeacon.jpg';
divItem.appendChild(image);
carouselIndicators.appendChild(liIndicator);
carouselInner.appendChild(divItem);
}
})();
I tried deferring the loading of the images too (the top code section hadn't had the onload event then):
function initImg() {
var imgs = document.querySelectorAll('#property_image_gallery .carousel-inner .item img');
for (var i = 0; i < imgs.length; i++) {
var imgSource = imgs[i].dataset.src;
imgs[i].src = imgSource;
}
}
window.onload = initImg
2 hours in. no results. I am stumped. What am I missing? how can I force the browser to just move on with life and load those images later on?
At first, you may load images one after one, using recursive functions:
function addimg(img){
img.onload=function(){
addimg(nextimg) ;
img.onload=null;//kill closure -> free the js memory
}
}
Start that if the html is loaded completely:
window.onload=addimg;
(pseudocode)
You can also use a image compressor tool to make the images load faster.
http://optimizilla.com/
This is a great article that might also help you
https://varvy.com/pagespeed/defer-images.html
Few suggestions:
If the images are not in the current viewport and are taking up too much initial bandwidth then i suggest to lazy load images when the user is in (or close to) the same viewport of the images.
You can also try deferring the images like what you are doing, but ensure the script is run right before the end body tag.
I also suggest doing things like making sure images are correctly compressed and resized (you have an image there that is 225kb which isnt ideal)
I'm trying to dynamically preload list of files which may be anything between images and JavaScript files. Everything is going supersmooth with Chrome and Firefox, but failing when I'm trying to preload JavaScript files with Edge. Edge still can handle images for example but no js files. And yes I've tried with addEventListener, it's not working either.
Edge doesn't give me any errors.
var object = {};
object = document.createElement('object');
object.width = object.height = 0;
object.data = path/to/javascriptfile.js
body.appendChild(object);
object.onload = function(){
console.log('hello world')
//not firing with edge
}
Anything relevant I'm missing?
UPDATE: Didn't get any success after the day. Will probably leave it for now and just skip preloading script files with edge until i find a solution.
Perhaps worth a check - from msdn:
The client loads applications, embedded objects, and images as soon as
it encounters the applet, embed, and img objects during parsing.
Consequently, the onload event for these objects occurs before the
client parses any subsequent objects. To ensure that an event handler
receives the onload event for these objects, place the script object
that defines the event handler before the object and use the onload
attribute in the object to set the handler.
https://msdn.microsoft.com/en-us/library/windows/apps/hh465984.aspx
Edit, a clarification:
You should attach the event listener before the element is added to the page.
Even doing that I'm not sure if it'll work or not though. But to make sure you've exhausted all options try the example below:
function doLoad() {
console.log('The load event is executing');
}
var object = {};
object = document.createElement('object');
object.width = object.height = 0;
object.data = 'path/to/javascriptfile.js';
object.onreadystatechange = function () {
if (object.readyState === 'loaded' || object.readyState === 'complete') doLoad();
console.log('onreadystatechange');
}
if (object.addEventListener) {
object.addEventListener( "load", doLoad, false );
console.log('addEventListener');
}
else
{
if (object.attachEvent) {
object.attachEvent( "onload", doLoad );
console.log('attachEvent');
} else if (object.onLoad) {
object.onload = doLoad;
console.log('onload');
}
}
var body = document.getElementsByTagName("body")[0];
body.appendChild(object);
If this doesn't work, you could perhaps preload using "image" instead of "object" in IE: https://stackoverflow.com/a/11103087/1996783
Working temporary solution is to put onload event directly to script element instead of object. It's sad since it works like a charm in Chrome & FF.
It turns out, object.data with css source did not load either. I don't know if it's a bug since it still can load image from to object.data.
But show must go on.
Cheers, eljuko
I want to implement a plug-in serial download pictures in MooTools. Let's say there are pictures with the img tag inside a div with the class imageswrapper. Need to consistently download each image after it loads the next and so on until all the images are not loaded.
window.addEvent('domready', function(){
// get all images in div with class 'imageswrapper'
var imagesArray = $$('.imageswrapper img');
var tempProperty = '';
// hide them and set them to the attribute 'data-src' to cancel the background download
for (var i=0; i<imagesArray.length; i++) {
tempProperty = imagesArray[i].getProperty('src');
imagesArray[i].removeProperty('src');
imagesArray[i].setProperty('data-src', tempProperty);
}
tempProperty = '';
var iterator = 0;
// select the block in which we will inject Pictures
var injDiv = $$('div.imageswrapper');
// recursive function that executes itself after a new image is loaded
function imgBomber() {
// exit conditions of the recursion
if (iterator > (imagesArray.length-1)) {
return false;
}
tempProperty = imagesArray[iterator].getProperty('data-src');
imagesArray[iterator].removeProperty('data-src');
imagesArray[iterator].setProperty('src', tempProperty);
imagesArray[iterator].addEvent('load', function() {
imagesArray[iterator].inject(injDiv);
iterator++;
imgBomber();
});
} ;
imgBomber();
});
There are several issues I can see here. You have not actually said what the issue is so... this is more of a code review / ideas for you until you post the actual problems with it (or a jsfiddle with it)
you run this code in domready where the browser may have already initiated the download of the images based upon the src property. you will be better off sending data-src from server directly before you even start
Probably biggest problem is: var injDiv = $$('div.imageswrapper'); will return a COLLECTION - so [<div.imageswrapper></div>, ..] - which cannot take an inject since the target can be multiple dom nodes. use var injDiv = document.getElement('div.imageswrapper'); instead.
there are issues with the load events and the .addEvent('load') for cross-browser. they need to be cleaned up after execution as in IE < 9, it will fire load every time an animated gif loops, for example. also, you don't have onerror and onabort handlers, which means your loader will stop at a 404 or any other unexpected response.
you should not use data-src to store the data, it's slow. MooTools has Element storage - use el.store('src', oldSource) and el.retrieve('src') and el.eliminate('src'). much faster.
you expose the iterator to the upper scope.
use mootools api - use .set() and .get() and not .getProperty() and .setProperty()
for (var i) iterators are unsafe to use for async operations. control flow of the app will continue to run and different operations may reference the wrong iterator index. looking at your code, this shouldn't be the case but you should use the mootools .each(fn(item, index), scope) from Elements / Array method.
Anyway, your problem has already been solved on several layers.
Eg, I wrote pre-loader - a framework agnostic image loader plugin that can download an array of images either in parallel or pipelined (like you are trying to) with onProgress etc events - see http://jsfiddle.net/dimitar/mFQm6/ - see the screenshots at the bottom of the readme.md:
MooTools solves this also (without the wait on previous image) via Asset.js - http://mootools.net/docs/more/Utilities/Assets#Asset:Asset-image and Asset.images for multiple. see the source for inspiration - https://github.com/mootools/mootools-more/blob/master/Source/Utilities/Assets.js
Here's an example doing this via my pre-loader class: http://jsfiddle.net/dimitar/JhpsH/
(function(){
var imagesToLoad = [],
imgDiv = document.getElement('div.injecthere');
$$('.imageswrapper img').each(function(el){
imagesToLoad.push(el.get('src'));
el.erase('src');
});
new preLoader(imagesToLoad, {
pipeline: true, // sequential loading like yours
onProgress: function(img, imageEl, index){
imgDiv.adopt(imageEl);
}
});
}());