Reputation: 552
Before I explain what I want to do, here's an MCV of what I'm coding
$("#button").submit(function(event) {
event.preventDefault();
var formData = new FormData(this);
var myString = $('#textarea').val();
var myRegexp = /src="blob:([^'"]+)"/gm;
match = myRegexp.exec(myString);
var inProgress = 0;
while (match != null) {
var xhr = new XMLHttpRequest();
addr = match[1];
xhr.open('GET', 'blob:' + addr, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var myBlob = this.response;
var data = new FormData();
data.append('file', myBlob);
$.ajax({
url: "uploader.php",
type: 'POST',
data: data,
contentType: false,
processData: false,
beforeSend: function() {
inProgress++;
},
success: function(data) {
myString = myString.replace("blob:" + addr, data);
},
error: function() {
// error
},
complete: function() {
--inProgress;
}
});
} else {
// error
}
};
xhr.send();
match = myRegexp.exec(myString);
}
if (!inProgress) {
formData.set('textarea', myString);
submitForm(formData);
}
});
So, I have a text area and it contains an unknown number of BLOB objects. I first try to find these BLOB objects using regexp and then I upload them to the server using a PHP file called uploader.php. Once the file is uploaded, it will return the URL of the uploaded file and I want to replace the BLOB URL by the URL of the uploaded file in the text area and then submit the final content of the textarea to the server for further processing.
It turns out that when I run the code, only the last instance of the regexp is replaced by its uploaded URL. The others remain as they were. I suspect that this is because the while loop does not wait for the ajax requests to be complete. I had a similar problem when trying to submit the form and I solved it by following the suggestions in this answer but I don't know how to fix this issue this time.
Any idea is appreciated
Update: I tried to make ajax work synchronously but my browser said that it was deprecated and it didn't work.
Upvotes: 0
Views: 486
Reputation: 3044
It seems you don't really need it to be synchronous (and I can't see a case when it's better to make an async action look synchronous), but rather only need it to be sequential.
It is possible to make async actions sequential by the use of callbacks (which are rewritable as Promise and in turn rewritable as async/await methods but I'll keep it simple):
// myString is made global for simplicity
var myString;
function uploadBlob(myBlob, addr, callback) {
var data = new FormData();
data.append('file', myBlob);
$.ajax({
url: "uploader.php",
type: 'POST',
data: data,
contentType: false,
processData: false,
success: function(data) {
// file uploaded OK, replace the blob expr by the uploaded file url(data)
myString = myString.replace("blob:" + addr, data);
callback();
},
error: function() {
// error, the uploaded most likely failed, we leave myString alone
// or alternatively replace the blob expr by empty string
// because maybe we dont want to post blob in the final form submit
// uncomment if needed
// myString = myString.replace("blob:" + addr, "");
callback();
}
});
}
function getBlobAndUpload(addr, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blob:' + addr, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var myBlob = this.response;
uploadBlob(myBlob, addr, callback);
} else {
// error, but callback anyway to continue processing
callback();
}
};
xhr.send();
}
function processAddresses(addresses, callback, current) {
var index = current || 0;
// all addresses processed?
if (index >= addresses.length) {
// yes no more address, call the callback function
callback();
} else {
var addr = addresses[index];
// once the get/upload is done the next address will be processed
getBlobAndUpload(addr, function() {
processAddresses(addresses, callback, index + 1);
});
}
}
$("#button").submit(function(event) {
event.preventDefault();
var formData = new FormData(this);
var addresses = [];
// initialize both localString and myString to the content of the textArea
// localString will be used to extract addresses,
// while myString will be mutated during the upload process
var localString = myString = $('#textarea').val();
var myRegexp = /src="blob:([^'"]+)"/gm;
match = myRegexp.exec(localString);
// collect all addresses first
while (match != null) {
addr = match[1];
addresses.push(addr);
match = myRegexp.exec(localString);
}
// initiate sequential processing of all addresses, and
// pass the callback function triggering the form submit
processAddresses(addresses, function() {
// all the successfully uploaded blob exprs in my string should
// be now replaced by the remote file url now (see commented part
// in upload blob error for a variation of the feature
formData.set('textarea', myString);
submitForm(formData);
});
});
Upvotes: 1
Reputation: 686
So. I said in comments, that you could use async/await, and gave links. Now I am going to try to teach you how to work with promises and XMLHttpRequest.
So first thing. I would use my own 'library' (not really a library, just 3 new command) called PromiseReq which has XMLHttpsRequest that returns Promises.
You would need two functions from it:
sendToServer(config, data)
and getServerFile(config)
. They do what their names implies.(sendToServer
is not so good at the time, but I will improve it sometime later). They just use Promises as returns. They work in very easy way. Code @ Github
BUT It was designed for my uses only, so it is not very flexible (although I hope I will improve it sometime).
So we need to learn how to use Promises.
Firstly you need to know what Promise is and why do we use it.
Then you can create one like this:
let pmq = new Promise((res,rej)=>{
// PROMISE BODY HERE
});
Here first warning. Promises made that way don't support return as a way to resolve Promise! You have to use res()
!
Some functions just return them (such as fetch()) and we can handle them right after calling function.
Now pmq
will be our promise.
You can use pmq.then(callback)
to handle what will happen if somewhere in promise body is res()
call and pmq.catch(callback)
to handle what happens when rej()
is called. Remember, that .catch(cb)
and .then(cb)
returns a Promise, so you can safely chain more than one .then()
and at the end add .catch()
and it will handle rejection from every one of .then()
s.
For example:
pmq = fetch("file.txt");
pmq.then(e=>console.log(e.json())).then(console.log).catch(console.error);
There is a big note there.
The order in which this events will fire.
If for example rP()
waits 1s than logs "A"
then resolves, this code:
let a = rP();
a.then(_=>console.log("B")).catch(console.error);
console.log("C");
will result in:
C
A
B
Becuase of this there is async/await needed to do this.
To do so you have to make an async function with keyword async
.
let fn = async ()=>{}
Here is second thing. Async functions ALWAYS return Promise. And that is the second way you can create a promise. You just don't use res()
, rej()
only return
and throw
.
Now we can call inside fn()
:
let a = await rP().then(_=>console.log("B")).catch(console.error);
console.log("C");
and we will get our
A
B
C
Now. How to use it with XMLHttpRequest?
You need to create new Promise with simple XMLHttpRequest inside:
let xmlp = (type, path,data) => return new Promise((res,req)=>{
let xhr = new XMLHttpsRequest();
xhr.open(type, path, true); // true implies that is it asynchronous call
//...
xhr.send(data);
});
And now when to resolve?
XMLHttpRequest
has useful event properties and events. The one that is best for our case is onreadystatechange
.
You can use it like so:
xhr.onreadystatechange = _=>{
if(xhr.readyState === 4 && xhr.status === 200) // Everything went smoothly
res(xhr.responseText);
else if(shr.readyState === 4 && xhr.status !== 200) // Something went wrong!
rej(xhr.status);
}
And then to get data you can either
Async/Await
// INSIDE ASYNC FUNCTION
let resData = await xmpl("GET", "path.txt", null).catch(console.error);
.then()
let resData;
xmpl("GET", "path.txt", null).then(r=>{
resData = r;
// REST OF WHOLE FUNCTION TO ENSURE THAT resData HAS BEEN SET
})
.catch(console.error);
You can also send data with xmpl()
.
xmpl("POST", "observer.php", "Data to send to observer.php!")
.then(whatToDoAfterSendFN);
/*
to get such data in PHP you have to use
$post = file_get_contents('php://input');
*/
I know that this answer is a bit messy and stuff, but I didn't have any idea how to write it :P Sorry.
Upvotes: 1