Reputation: 4885
I have got the following script I use to pre-load images:
export default function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
});
}
return Promise.all(images.map(src => loadImage(src)));
}
I then use this within a Vue component like follows:
PreloadImages(this.images).then(() => (this.imagesPreloaded = true));
I want to be able to get progress from this however within my component so I can display for example 2/50 Images Loaded
. How would I go about doing this?
This is what I ended up with:
PreloadImages.js
export default function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
});
}
return images.map(src => loadImage(src));
}
Within my component:
handlePreload() {
PreloadImages(this.images).forEach(p => p.then(() => {
this.imagesPreloaded++;
}));
},
Upvotes: 4
Views: 1039
Reputation: 13376
... tldr ...
There will be 2 different basic ideas and 4 solutions for demonstration purpose.
The first two approaches are valid in case one can not or is not allowed to change the return value (a single promise) of PreloadImages
as from the OP's originally provided example code.
The 3rd example presents the preferred approach which is to change the return value of PreloadImages
to a list of (image preloading) promises, whereas the 4th example is just No.3 that in addition features a real live demo of progress-bar and statistics rendering.
One could use a Proxy
object that one, in addition to the image source list, does provides to the preloadImages
function.
One would use such an object in its set and validate configuration.
Choosing a Proxy
based approach comes with following advantages ...
eventProxy
into preloadImages
makes this new implementation agnostic to later user cases.preloadImages
' scope unknown use case, that later needs to be handled, will be provided as part of the eventProxy
./*export default */function preloadImages(imageSourceList, eventProxy) {
// - the introduction of an `eventProxy` makes this implementation
// agnostic to later user cases.
// - one just needs to implement the necessary api that provides
// information about image loading states.
// - the user specific, but in this scope unknown use case, that later
// needs to be handled, will be provided as part of the `eventProxy`.
const maxCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(image);
++successCount;
eventProxy.event = {
image,
maxCount,
successCount,
failureCount
}
};
image.onerror = image.onabort = function() {
reject(src);
++failureCount;
eventProxy.event = {
image,
maxCount,
successCount,
failureCount
}
};
image.src = src;
});
}
return Promise.all(imageSourceList.map(src => loadImage(src)));
}
function renderImageLoadProgress(evt) {
// the render functionality goes in here ...
console.log('renderImageLoadProgress :: evt :', evt);
}
function isImageLoadType(type) {
return (
type
&& ('image' in type)
&& ('maxCount' in type)
&& ('successCount' in type)
&& ('failureCount' in type)
)
}
const loadEventValidator = {
set: function(obj, key, value) {
let isSuccess = false;
if ((key === 'event') && isImageLoadType(value)) {
obj[key] = value;
isSuccess = true;
// provide render functionality as part of
// the proxy's correct setter environment.
renderImageLoadProgress(value);
}
return isSuccess;
}
}
const loadEventProxy = new Proxy({}, loadEventValidator);
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
preloadImages(imageSourceList, loadEventProxy);
.as-console-wrapper { min-height: 100%!important; top: 0; }
In case of not being able making use of proxies, one easily can switch the above approach to EventTarget
and CustomEvent
, especially since there are polyfills available that also do work in older versions of Internet Explorer. The idea behind this solution and its advantages are the same as with the former Proxy
based one ...
/*export default */function preloadImages(imageSourceList, eventTarget) {
// - additionally injecting/providing an `eventTarget` makes this
// implementation agnostic to later user cases.
// - one just needs to dispatch all relevant information via the
// `details` property of a custom event.
// - the user specific, but in this scope unknown use case, will be
// handled from ouside via listening to the custom event's type.
// - there are polyfills for `EventTarget` as well as for `CustomEvent`
// that also do work in older versions of Internet Explorer.
const maxCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(image);
++successCount;
eventTarget.dispatchEvent(new CustomEvent("loadstatechange", {
detail: {
image,
maxCount,
successCount,
failureCount
}
}));
};
image.onerror = image.onabort = function() {
reject(src);
++failureCount;
eventTarget.dispatchEvent(new CustomEvent("loadstatechange", {
detail: {
image,
maxCount,
successCount,
failureCount
}
}));
};
image.src = src;
});
}
return Promise.all(imageSourceList.map(src => loadImage(src)));
}
function renderImageLoadProgress(evt) {
// the render functionality goes in here ...
console.log('renderImageLoadProgress :: evt.detail :', evt.detail);
}
const loadEventTarget = new EventTarget();
loadEventTarget.addEventListener('loadstatechange', renderImageLoadProgress);
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
preloadImages(imageSourceList, loadEventTarget);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Note
Since the 2 above given approaches did not change the return value/signature of the original implementation of PreloadImages
they had to be implemented exactly each in their own way. But both together come with a lot of additional code, the proxy based approach being a little fatter than the event based one.
Anyhow, if one is/was just willing to change the return value of the former PreloadImages
to an array of image loading promises one could achieve a very clean and lean implementation that might look similar to the next one ...
/*export default */function getAsyncLoadingImageList(imageSourceList) {
const totalCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({
image,
counts: {
total: totalCount,
success: ++successCount,
failure: failureCount
},
success: true,
});
};
image.onerror = image.onabort = function() {
reject({
image,
counts: {
total: totalCount,
success: successCount,
failure: ++failureCount
},
success: false,
});
};
image.src = src;
});
}
// return list of *image loading* promises.
return imageSourceList.map(src => loadImage(src));
}
function renderImageLoadProgress(imageLoadData) {
// the render method.
console.log('imageLoadData : ', imageLoadData);
}
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
getAsyncLoadingImageList(imageSourceList).forEach(promise =>
promise
.then(renderImageLoadProgress)
.catch(renderImageLoadProgress)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
In order to present a real progress bar demo, the 3rd example, that already had it all, was taken as is, but now features a full blown render method instead of just logging the image-loading progress-data ...
/*export default */function getAsyncLoadingImageList(imageSourceList) {
const totalCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({
image,
counts: {
total: totalCount,
success: ++successCount,
failure: failureCount
},
success: true,
});
};
image.onerror = image.onabort = function() {
reject({
image,
counts: {
total: totalCount,
success: successCount,
failure: ++failureCount
},
success: false,
});
};
image.src = src;
});
}
// return list of *image loading* promises.
return imageSourceList.map(src => loadImage(src));
}
function getImageLoadProgressRenderConfig(progressContainer) {
const elmProgress = progressContainer.querySelector('progress');
const countsContainer = progressContainer.querySelector('.image-counts');
const failuresContainer = progressContainer.querySelector('.load-failures');
const elmImageCount = countsContainer.querySelector('.count');
const elmImageTotal = countsContainer.querySelector('.total');
const elmFailureCount = failuresContainer.querySelector('.failure-count');
const elmCurrentCount = failuresContainer.querySelector('.current-count');
return {
nodes: {
progress: {
display: elmProgress,
count: elmImageCount,
total: elmImageTotal
},
failure: {
display: failuresContainer,
failureCount: elmFailureCount,
currentCount: elmCurrentCount
}
},
classNames: {
failure: 'failures-exist'
}
};
}
function renderImageLoadProgressViaBoundConfig(imageLoadData) {
const imageCounts = imageLoadData.counts;
const imageCountTotal = imageCounts.total;
const imageCountSuccess = imageCounts.success;
const imageCountFailure = imageCounts.failure;
const imageCountCurrent = (imageCountSuccess + imageCountFailure);
const renderConfig = this;
const renderNodes = renderConfig.nodes;
const progressNodes = renderNodes.progress;
const failureNodes = renderNodes.failure;
const classNameFailure = renderConfig.classNames.failure;
const isFailureOnDisplay =
failureNodes.display.classList.contains(classNameFailure);
progressNodes.display.textContent = `${ imageCountCurrent } \/ ${ imageCountTotal }`;
progressNodes.display.value = imageCountCurrent;
progressNodes.count.textContent = imageCountCurrent;
if (!imageLoadData.success) {
if (!isFailureOnDisplay) {
failureNodes.display.classList.add(classNameFailure);
}
failureNodes.failureCount.textContent = imageCountFailure;
} else if (isFailureOnDisplay) {
failureNodes.currentCount.textContent = imageCountCurrent
}
}
function preloadImagesAndRenderLoadProgress(imageSourceList, progressContainer) {
const totalImageCount = imageSourceList.length;
const renderConfig = getImageLoadProgressRenderConfig(progressContainer);
const renderNodes = renderConfig.nodes;
const progressNodes = renderNodes.progress;
const failureNodes = renderNodes.failure;
const renderImageLoadProgress =
renderImageLoadProgressViaBoundConfig.bind(renderConfig);
failureNodes.display.classList.remove(renderConfig.classNames.failure);
failureNodes.failureCount.textContent = 0;
failureNodes.currentCount.textContent = 0;
progressNodes.display.max = totalImageCount;
progressNodes.total.textContent = totalImageCount;
renderImageLoadProgress({
counts: {
success: 0,
failure: 0
},
success: true,
});
getAsyncLoadingImageList(imageSourceList).forEach(promise => promise
.then(renderImageLoadProgress)
.catch(renderImageLoadProgress)
);
}
const imageSourceListWithFailure = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563',
'https://picsum.photos/id/1018', // loading will fail.
'https://picsum.photos/id/1018/3914/2935',
'https://picsum.photos/id/1019/5472/3648',
'https://picsum.photos/id/1020/4288/2848',
'https://picsum.photos/id/1021', // loading will fail.
'https://picsum.photos/id/1021/2048/1206',
'https://picsum.photos/id/1022/6000/3376',
'https://picsum.photos/id/1023/3955/2094'
];
const imageSourceListWithoutFailure = [
'https://picsum.photos/id/1039/6945/4635',
'https://picsum.photos/id/1038/3914/5863',
'https://picsum.photos/id/1037/5760/3840',
'https://picsum.photos/id/1036/4608/3072',
'https://picsum.photos/id/1035/5854/3903',
'https://picsum.photos/id/1033/2048/1365',
'https://picsum.photos/id/1032/2880/1800',
'https://picsum.photos/id/1031/5446/3063',
'https://picsum.photos/id/1029/4887/2759',
'https://picsum.photos/id/1028/5184/3456',
'https://picsum.photos/id/1027/2848/4272',
'https://picsum.photos/id/1026/4621/3070'
];
preloadImagesAndRenderLoadProgress(
imageSourceListWithFailure,
document.querySelector('#loading-with-failure')
);
preloadImagesAndRenderLoadProgress(
imageSourceListWithoutFailure,
document.querySelector('#loading-without-failure')
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
.image-loading-progress {
margin: 20px 0;
}
.image-loading-progress,
.image-loading-progress > span,
.image-loading-progress progress {
display: block;
width: 100%;
}
.image-loading-progress > .load-failures {
display: none
}
.image-loading-progress > .load-failures.failures-exist {
display: block
}
<label class="image-loading-progress" id="loading-with-failure">
<progress value="0" max="0">0 / 0</progress>
<span class="image-counts">
...
<span class="count">0</span>
out of
<span class="total">0</span>
images finished loading ...
</span>
<span class="load-failures">
...
<span class="failure-count">0</span>
out of
<span class="current-count">0</span>
images are not available ...
</span>
</label>
<label class="image-loading-progress" id="loading-without-failure">
<progress value="0" max="0">0 / 0</progress>
<span class="image-counts">
...
<span class="count">0</span>
out of
<span class="total">0</span>
images finished loading ...
</span>
<span class="load-failures">
...
<span class="failure-count">0</span>
out of
<span class="current-count">0</span>
images are not available ...
</span>
</label>
Upvotes: 0
Reputation: 4267
This is how you should approach it, don't introduce any extra looping:
var imageSrcList = [
"https://image.flaticon.com/icons/svg/565/565860.svg",
"https://image.flaticon.com/icons/svg/565/565861.svg",
"https://Error.Done.One.Purpose",
"https://image.flaticon.com/icons/svg/565/565855.svg",
];
var imageContainer = document.getElementById("loadedImages");
var loadedCount = 0;
function displayCurrentLoadStatus(){
console.log("Current load progress: ", loadedCount, " of ", imageSrcList.length);
}
function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = function()
{
++loadedCount;
imageContainer.appendChild(img);
displayCurrentLoadStatus();
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
});
}
// Note that I removed Promise.all, let's just return a list of promises for the images passed
return images.map((imgSrc, i)=> loadImage(imgSrc).catch((rejectedSrcError=> rejectedSrcError)));
}
// now let's create all promises for each image in images
Promise.all(PreloadImages(imageSrcList)).then( resolvedSrcList =>
{
// Use your resolved Src's here
console.log("Load complete: ",loadedCount, "/",resolvedSrcList.length, " images were loaded successfully.");
});
img{
width: 60px;
height: 60px;
display: inline-block;
}
<div id="loadedImages"></div>
Upvotes: 0
Reputation: 1043
try this:
function PreloadImages(images) {
let promises = [];
images.map(src => loadImage(src));
function loadImage(src) {
promises.push(new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
}));
}
Promise.all(promises);
return promises;
}
let images = [];
let counter = 0;
let promises = PreloadImages(images);
promises.forEach(p => p.then(() => {
counter++;
console.log(counter);
}));
Upvotes: 1