0

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.

7
  • Have you tried async/await with fetch? I don't work with jQuery (I think, that pure JS is best) so I don't know if jQuery's $.ajax works with Promises. Commented Mar 15, 2019 at 17:49
  • @SkillGG - the OP uses BOTH vanilla AND jQuery :-/) Commented Mar 15, 2019 at 17:50
  • Without vanila there is no jQuery :) Commented Mar 15, 2019 at 17:51
  • Possible duplicate of Getting Data from Ajax request displayed Commented Mar 15, 2019 at 17:52
  • I would use fetch() and XMLHttpRequest (with Header) changed slightly to return Promise and wait with await for it. But I would not use $.ajax, so I have no idea if this solution fits OP. Commented Mar 15, 2019 at 17:54

2 Answers 2

1

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);
  });
});

Sign up to request clarification or add additional context in comments.

13 Comments

I need some time to analyze your answer and understand it. Thank you for taking the time to write an answer.
The answer won't work as it stands, I'm trying to figure how to deal with myString
It should be OK now. I think I've understood how myString was meant to be used and used a different local variable for blob parsing. Also the in progress var was not needed anymore.
Thanks. I tried your approach, though I had to fix some issues that didn't really change the logic of your code. For example, the array addresses had not been defined in $('#button').submit() or XHR had not been defined in getBlobAndUpload(). I also changed callBack() to callback() which seemed to be the intended one. But it didn't work. It still replaces the last instance of a BLOB object only and ignores the rest. :(
I found the problem. uploadBlob() needed three variables: myBlob, addr, callback. Before this, addr was not updated and it replaced the same thing over and over again.
|
1

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

  1. Async/Await
// INSIDE ASYNC FUNCTION
let resData = await xmpl("GET", "path.txt", null).catch(console.error);
  1. .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.

2 Comments

I need some time to analyze your answer and understand it. Thank you for taking the time to write an answer.
This is a very good and detailed answer.But I'm afraid my current knowledge of JavaScript is not good enough to fully understand and appreciate it. :( I learned a lot from it though and I'll return to it in future many times again for sure. Thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.