JS Wait for few events in a function to complete - javascript

I have a function which uploads images and displays it on the page. How should I do to wait for the function to complete?
$("#images").change(function(){
updateArray(this);
addImages();
});
function updateArray(inp)
{
for(var i=0,file; file = inp.files[i]; i++)
{
var reader = new FileReader();
reader.onload = function (evt)
{
images[images.length] = evt.target.result;
}
reader.readAsDataURL(file);
}
}
I want to call addImages() after all events in updateArray() are completed

I would use jQuery's deferred/promises here. This will allow you to wait until each onload is complete, and then trigger a callback.
Here's an example:
function updateArray(inp)
{
var promises = [];
for(var i=0,file; file = inp.files[i]; i++)
{
var reader = new FileReader(),
d = new $.Deferred();
promises.push(d);
// Make sure we "capture" the correct 'd'
(function(d){
reader.onload = function (evt)
{
images.push(evt.target.result);
d.resolve();
}
}(d));
reader.readAsDataURL(file);
}
return $.when.apply($, promises);
}
This will create a new deferred object for each FileReader. When each one finishes, its respective deferred will be resolved. The $.when is used to combine all of the deferreds into one. So, in your .change(), you can simply do this:
$("#images").change(function(){
updateArray(this).done(function(){
addImages();
});
});
P.S. You can also pass parameters to resolve() to be used in the .done() callback. So, instead of appending to the images array, you can do d.resolve(evt.target.result);.
(function(d){
reader.onload = function (evt)
{
d.resolve(evt.target.result);
}
}(d));
Then in .done(), you can do:
$("#images").change(function(){
updateArray(this).done(function(){
// Each '.resolve' added a new parameter to here, let's get them all
images = [].slice.call(arguments, 0);
addImages();
});
});

There are a couple of ways that you can approach this, however if you're using jQuery (which you appear to be) I'd recommend looking into promises (see: http://api.jquery.com/promise/).
They are a more modern paradigm for deferred callbacks which have some semantic processing for executing functions based on successful completion, failure, and those which will always process.

Related

JavaScript Onload property

I have read some posts regarding the onload property. My understanding is that it is not a method, not an event, event listener, or trigger. It is merely a empty pointer to a function waiting to be assigned. Here is a shortened script. It assigns a image manipulation function upon event then renders the image to the web page. Instead of assign the function to onload which is the pointer to the function. Why can't I just execute the function directly? Am I making any sense here? Thanks
var reader = new FileReader();
reader.onload = function(e) {
var img = document.createElement("img");
img.src = e.target.result;
var canvas = document.createElement("canvas");
......
dataurl = canvas.toDataURL(file.type);
document.getElementById('output').src = dataurl;
}
reader.readAsDataURL(file);
Accordingly to MDN:
The FileReader.onload property contains an event handler executed when
the load event is fired, when content read with readAsArrayBuffer,
readAsBinaryString, readAsDataURL or readAsText is available.
So it's executed after reading content is done, that you're performing by calling reader.readAsDataURL(file).
onload is a field of the object FileReader which handles a reference to the function that you wanna execute when the desired file gets loaded. Essentially, it's the callback that gets called when the event load is triggered. Why using this pattern and not executing the function directly? Because loading files is an asynchronous task (which is initialized with readAsDataURL, readAsBinaryString, readAsArrayBuffer or readAsText).
You can't call the function directly because files load asynchronously. You can convert the FileReader code to return a promise though and then you can use async/await to make the code look like you're directly calling it
funciton readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(e.target.result);
reader.onerror = reject;
reader.readAsDataURL(file);
};
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image():
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
};
}
async function foo() {
const result = await readFileAsDataURL(file);
const img = await loadImage(result);
// you can now use img
......
}
So in essence, OnLoad is an event handler (or reference to the event handler) for FileReader when it is called to further processes data when FileReader is done loading? Is my interpretation correct?

How to determine that all the files have been read and resolve a promise

The following code is responsible for reading files. My requirement is how to find whether all files has been read so that I can return or resolve a promise from the parent function(readmultifiles).
$.when(readmultifiles(files))
.then(function(){//all files uploaded}))
Above code initiates the file read. What can be done so that upon reading of all files
callback is done or a return can be made.
function readmultifiles(files) {
// Read first file
setup_reader(files, 0);
}
function setup_reader(files, i) {
var file = files[i];
var name = file.name;
var reader = new FileReader();
reader.onload = function(e) {
readerLoaded(e, files, i, name);
};
reader.readAsBinaryString(file);
// After reading, read the next file.
}
function readerLoaded(e, files, i, name) {
// get file content
var bin = e.target.result;
// do sth with text
// If there's a file left to load
if (i < files.length - 1) {
// Load the next file
setup_reader(files, i + 1);
}
}
There are several things to consider in a good design using promises that your implementation could learn from:
Create a promise (called "promisify") from the lowest level async operation your have. Then, you can use promise features to control the logic flow and propagate errors and your code will be consistently implemented with promises. In this case, it means you should promisify readFile(). It also makes readFile() more useful elsewhere in your project or in future projects.
Make sure you are always propagating errors properly. With async code when not using promises, it can be hard to properly get errors back to the original caller, particular if the async logic ends up complicated (with nested or sequences operations).
Consider carefully whether your async operations must be sequences or whether they can run in parallel. If one operation does not depend upon another and you aren't likely to overload some service with multiple requests, then running things in parallel will often achieve a result quicker.
Return promises from async functions so callers can know when things are done and can access async results.
Don't create another promise around an existing promise unnecessarily (considered one of the promise anti-patterns).
If using jQuery promises, try to stick to jQuery features that are compatible with the promise standard so you don't run into interoperability issues going forward or confuse future readers of your code who are more likely to know how standard promises work.
Given all that, here are five ways to implement your code -using standard promises, using jQuery promises and with your operation sequences or run in parallel and using Bluebird promises. In all cases, you get an array of results in order at the end.
Promisify readFile() using standard promises
First, let's "promisify" your readFile operation so you can then use promise logic to control things.
function readFile(file) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function(e) {
resolve(e.target.result);
};
reader.onerror = reader.onabort = reject;
reader.readAsBinaryString(file);
});
}
With standard promises, all operation in parallel
To run all your file operations in parallel and return all the results in order and use standard promises, you can do this:
function readmultifiles(files) {
return Promise.all(files.map(readFile));
}
// sample usage
readmultifiles(arrayOfFiles).then(function(results) {
// all results in the results array here
});
With standard promises, all operations in sequence
To run all your files operations in sequence (which it does not look like you need to do here because all the operations are indepedent even though your original code was sequencing them) and return all the results in order and use standard promises, you can do this.
This, somewhat standard design pattern for sequencing uses .reduce() to sequence through the array and chain all the operations together so they are run one at a time down the sequence of the chain:
function readmultifiles(files) {
var results = [];
files.reduce(function(p, file) {
return p.then(function() {
return readFile(file).then(function(data) {
// put this result into the results array
results.push(data);
});
});
}, Promise.resolve()).then(function() {
// make final resolved value be the results array
return results;
});
}
// sample usage
readmultifiles(arrayOfFiles).then(function(results) {
// all results in the results array here
});
And, here's how it would look using jQuery promises
Promisify readFile() using jQuery promises:
function readFile(file) {
return new $.Deferred(function(def) {
var reader = new FileReader();
reader.onload = function() {
def.resolve(e.target.result);
};
reader.onerror = reader.onabort = def.reject;
reader.readAsBinaryString(file);
}).promise();
}
Run in a parallel with jQuery:
function readmultifiles(files) {
return $.when.apply($, files.map(readFile));
}
// sample usage
readmultifiles(arrayOfFiles).then(function() {
var results = Array.prototype.slice(arguments);
// all results in the results array here
});
And, to run in sequence with jQuery
function readmultifiles(files) {
var results = [];
files.reduce(function(p, file) {
return p.then(function() {
return readFile(file).then(function(data) {
// put this result into the results array
results.push(data);
});
});
}, $.Deferred().resolve()).then(function() {
// make final resolved value be the results array
return results;
});
}
// sample usage
readmultifiles(arrayOfFiles).then(function(results) {
// all results in the results array here
});
Bluebird implementation
And, for completeness, I'll show you what it looks like using a little more advanced promise library like Bluebird that has additional capabilities that are useful here. The parallel code and the implementation of readFile() is the same as for standard promises, but for the sequential implementation, it could take advantage of some built-in Bluebird operations for sequencing async operation and it would just consist of:
function readmultifiles(files) {
return Promise.mapSeries(files, readFile);
}
// sample usage
readmultifiles(arrayOfFiles).then(function(results) {
// all results in the results array here
});
What if I change the code structure to this
$.when(readmultifiles(files)).then(
function(status) {
alert(status + ", things are going well");
},
function(status) {
alert(status + ", you fail this time");
},
function(status) {
$("body").append(status);
}
);
function readmultifiles(files) {
var dfrd = $.Deferred();
// Read first file
setup_reader(files, 0);
function setup_reader(files, i) {
var file = files[i];
var name = file.name;
var reader = new FileReader();
reader.onload = function(e) {
readerLoaded(e, files, i, name);
};
reader.readAsBinaryString(file);
// After reading, read the next file.
}
function readerLoaded(e, files, i, name) {
// get file content
var bin = e.target.result;
// do sth with text
namee.push(name);
// If there's a file left to load
if (i < files.length - 1) {
// Load the next file
setup_reader(files, i + 1);
} else {
dfrd.resolve(namee.join(','));
}
}
return dfrd.promise();
}

How can I access the callback arguments of an arbitrary amount of $.get() callbacks when using $.when().then()? [duplicate]

I am currently trying to build a File Uploader using the HTML5 FileAPI. The File Uploader is supposed to handle multiple files and show image previews if the file is an image.
since the FileReader class works asynchronously I want to wait until all the the files have been read. Therefore I am using Deferreds.
A method which reads the file is returning a promise. Another method loops through all files and pushes all promises into an array. Then I'm applying the then() method once all promises have been added to my array.
Now to my problem. Since the then() method is only called once, when I got all promises. I have no chance to process every single promise. What I want to do is, to loop through all my promises once they are all there and return the result.
This is my FileProcessor Object
read: function(file) {
var reader = new FileReader();
var deferred = $.Deferred();
reader.onload = function(event){
deferred.resolve(event.target.result);
};
reader.onerror = function() {
deferred.reject(this);
}
if(/^image/.test(file.type))
reader.readAsDataURL(file);
return deferred.promise();
},
And here comes the FileManager Object's handleFileSelect() method that is supposed to call the FileProcessor.read() method.
handleFileSelect: function(e){
var $fileList = $('#file-list');
var files = e.target.files;
var promises = []; //Promises are going to be stored here
var filestring = '';
var processedFile;
// Loop trough all selected files
for(var i = 0; i < files.length; i++){
// Store the promise in a var...
processedFile = FileProcessor.read(files[i]);
// And push it into the array where the promises are stored
promises.push(processedFile);
}
// Once all deferreds have been fired...
$.when.apply(window, promises).then(function(){
for(var i = 0; i < promises.length; i++){
// PROBLEM HERE: I need to get the
// result from my resolved Deferred
// of every single promise object
console.log(promises[i]);
}
});
},
Am I using the wrong approach for deferreds and promises? Aren't they supposed to be used like I'm trying to do and is there a more elegant way to achieve my purpose?
Am I using the wrong approach for deferreds and promises?
Yes. In the asynchronous callback you should not access promises, but only the callback arguments. The return promise of $.when does resolve with an array of the .resolve() parameters for each of the promises in the array - you can loop over the arguments object to access the results:
$.when.apply($, promises).then(function () {
for (var i = 0; i < arguments.length; i++) {
var singleresult = arguments[i][0];
console.log(singleresult);
}
});
you can use the arguments object to refer all the values returned by the promise objects
$.when.apply($, promises).then(function () {
for (var i = 0; i < arguments.length; i++) {
var data = arguments[i][0];
// PROBLEM HERE: I need to get the
// result from my resolved Deferred
// of every single promise object
console.log(data);
}
});
I see you heve accepted an answer but you might like to be aware that your handleFileSelect method can be significantly simplified as follows
handleFileSelect: function(e) {
var promises = $.map(e.target.files, function(file) {
return FileProcessor.read(file);
});
$.when.apply(window, promises).then(function() {
$.each(arguments, function(i, a) {
console.log(a[0]);
});
});
},
Of course it will get more complicated again when you flesh out the result handling but this should give you a nice concise basis.

Call a method once all deferred completes?

I have this class:
(function(){
"use strict";
var FileRead = function() {
this.init();
};
p.read = function(file) {
var fileReader = new FileReader();
var deferred = $.Deferred();
fileReader.onload = function(event) {
deferred.resolve(event.target.result);
};
fileReader.onerror = function() {
deferred.reject(this);
};
fileReader.readAsDataURL(file);
return deferred.promise();
};
lx.FileRead = FileRead;
}(window));
The class is called in a loop:
var self = this;
$.each(files, function(index, file){
self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
});
My question is, is there a way to call a method once the loop has completed and self.fileRead has returned it's deferred for everything in the loop?
I want it to call the method even if one or more of the deferred fails.
$.when lets you wrap up multiple promises into one. Other promise libraries have something similar. Build up an array of promises returned by fileRead.read and then pass that array to $.when and hook up then/done/fail/always methods to the promise returned by .when
// use map instead of each and put that inside a $.when call
$.when.apply(null, $.map(files, function(index, file){
// return the resulting promise
return self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
}).done(function() {
//now everything is done
})
var self = this;
var processFiles = function (data) {
var promises = [];
$.each(files, function (index, file) {
var def = data.fileRead.read(file);
promises.push(def);
});
return $.when.apply(undefined, promises).promise();
}
self.processFiles(self).done(function(results){
//do stuff
});
$.when says "when all these promises are resolved... do something". It takes an infinite (variable) number of parameters. In this case, you have an array of promises;
I know this is closed but as the doc states for $.when: In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when immediately fires the failCallbacks for its master Deferred. (emphasis on immediately is mine)
If you want to complete all Deferreds even when one fails, I believe you need to come up with your own plugin along those lines below. The $.whenComplete function expects an array of functions that return a JQueryPromise.
var whenComplete = function (promiseFns) {
var me = this;
return $.Deferred(function (dfd) {
if (promiseFns.length === 0) {
dfd.resolve([]);
} else {
var numPromises = promiseFns.length;
var failed = false;
var args;
var resolves = [];
promiseFns.forEach(function (promiseFn) {
try {
promiseFn().fail(function () {
failed = true;
args = arguments;
}).done(function () {
resolves.push(arguments);
}).always(function () {
if (--numPromises === 0) {
if (failed) {
//Reject with the last error
dfd.reject.apply(me, args);
} else {
dfd.resolve(resolves);
}
}
});
} catch (e) {
var msg = 'Unexpected error processing promise. ' + e.message;
console.error('APP> ' + msg, promiseFn);
dfd.reject.call(me, msg, promiseFn);
}
});
}
}).promise();
};
To address the requirement, "to call the method even if one or more of the deferred fails" you ideally want an .allSettled() method but jQuery doesn't have that particular grain of sugar, so you have to do a DIY job :
You could find/write a $.allSettled() utility or achieve the same effect with a combination of .when() and .then() as follows :
var self = this;
$.when.apply(null, $.map(files, function(index, file) {
return self.fileRead.read(file).then(function(fileB64) {
self.fileShow(file, fileB64, fileTemplate);
return fileB64;//or similar
}, function() {
return $.when();//or similar
});
})).done(myMethod);
If it existed, $.allSettled() would do something similar internally.
Next, "in myMethod, how to distinguish the good responses from the errors?", but that's another question :)

Looping through jQuery Deferreds after all promises have been called

I am currently trying to build a File Uploader using the HTML5 FileAPI. The File Uploader is supposed to handle multiple files and show image previews if the file is an image.
since the FileReader class works asynchronously I want to wait until all the the files have been read. Therefore I am using Deferreds.
A method which reads the file is returning a promise. Another method loops through all files and pushes all promises into an array. Then I'm applying the then() method once all promises have been added to my array.
Now to my problem. Since the then() method is only called once, when I got all promises. I have no chance to process every single promise. What I want to do is, to loop through all my promises once they are all there and return the result.
This is my FileProcessor Object
read: function(file) {
var reader = new FileReader();
var deferred = $.Deferred();
reader.onload = function(event){
deferred.resolve(event.target.result);
};
reader.onerror = function() {
deferred.reject(this);
}
if(/^image/.test(file.type))
reader.readAsDataURL(file);
return deferred.promise();
},
And here comes the FileManager Object's handleFileSelect() method that is supposed to call the FileProcessor.read() method.
handleFileSelect: function(e){
var $fileList = $('#file-list');
var files = e.target.files;
var promises = []; //Promises are going to be stored here
var filestring = '';
var processedFile;
// Loop trough all selected files
for(var i = 0; i < files.length; i++){
// Store the promise in a var...
processedFile = FileProcessor.read(files[i]);
// And push it into the array where the promises are stored
promises.push(processedFile);
}
// Once all deferreds have been fired...
$.when.apply(window, promises).then(function(){
for(var i = 0; i < promises.length; i++){
// PROBLEM HERE: I need to get the
// result from my resolved Deferred
// of every single promise object
console.log(promises[i]);
}
});
},
Am I using the wrong approach for deferreds and promises? Aren't they supposed to be used like I'm trying to do and is there a more elegant way to achieve my purpose?
Am I using the wrong approach for deferreds and promises?
Yes. In the asynchronous callback you should not access promises, but only the callback arguments. The return promise of $.when does resolve with an array of the .resolve() parameters for each of the promises in the array - you can loop over the arguments object to access the results:
$.when.apply($, promises).then(function () {
for (var i = 0; i < arguments.length; i++) {
var singleresult = arguments[i][0];
console.log(singleresult);
}
});
you can use the arguments object to refer all the values returned by the promise objects
$.when.apply($, promises).then(function () {
for (var i = 0; i < arguments.length; i++) {
var data = arguments[i][0];
// PROBLEM HERE: I need to get the
// result from my resolved Deferred
// of every single promise object
console.log(data);
}
});
I see you heve accepted an answer but you might like to be aware that your handleFileSelect method can be significantly simplified as follows
handleFileSelect: function(e) {
var promises = $.map(e.target.files, function(file) {
return FileProcessor.read(file);
});
$.when.apply(window, promises).then(function() {
$.each(arguments, function(i, a) {
console.log(a[0]);
});
});
},
Of course it will get more complicated again when you flesh out the result handling but this should give you a nice concise basis.

Categories