0

I am currently learning vanilla JS before jumping into any major frameworks so I can get the grip with HTTP request mechanics. I am sure there are a ton of better JS libraries for HTTP requests but I want to figure out how to solve my problem using classic XMLHttpRequest(), therefore please do not suggest any solutions that do not include XMLHttpRequest().

I am trying to loop through an array I have and make HTTP GET requests to an API with the information in the array and then populate my HTML with the response data. The code is quite simple:

Function that takes in my array of numbers:

function loadUniqueProjs(uniqueArray)
{

    var reqObject = [];

    for (var i in unique_array) 
    {
         outsideLoopRequest(unique_array[i], reqObject, i);
    }


}

I loop through the array and execute a function that is supposed to execute my GET request:

function outsideLoopRequest(arrayValue,reqObject, i){

    // Create XHR Object
    reqObject[i] = new XMLHttpRequest();
    reqObject[i].open('GET', 'http://localhost:3000/resource/Projects/' + arrayValue, true)

    reqObject[i].onload = function() {
        if (this.status == 200) {
            var client = JSON.parse(this.responseText);
            var output = '';

            for (var j in client){

                output +=   '<div class="card">'+
                                    '<h5 class="card-header" role="tab" id="heading'+ j + '">'+
                                        '<a data-toggle="collapse" data-parent="#accordion" style="color:black"' + 
                                            'href="#collapse' + j + '"aria-expanded="false" aria-controls="collapse' + j + '"class="d-block collapsed">' +
                                            '<i class="fa fa-angle-double-down pull-right"></i>#' +
                                            client[j].projectTitle + ' | ' + 'PM: ' + client[j].projectManager + ' | ' +
                                            'PO: ' + client[j].projectOwner + ' | ' + 'Estimated Deadline: ' + client[j].predictedCompletion +
                                            ' | ' + 'Status: ' + client[j].status +
                                            ' | ' + 'Requestor: ' + client[j].requestor +
                                        '</a>'+
                                    '</h5>'+
                            '</div>';
            }
    }

    document.getElementById("spinner").hidden = true;
    // output the data into the HTML page
    document.getElementById('accordion').innerHTML = output;

    console.log(this.status + ' Data retrieved successfully!');

}
    reqObject[i].send();
}

After hours of step by step debugging I learned that the HTTP requests are asynchronous and when utilising a loop to execute the requests one by one they won't behave like you want them to. The requests do not get executed one by one adding the HTML I require, instead the loop opens all requests first and then executes as they come, and when stepping through code in the web debugger the code jumps around all over the place it gets extremely confusing (sorry for rant).

I would like it to behave step by step. I did research on SO and someone suggested that these are scoping issues and that the requests should be made in a separate function, which is why I have structured my code with the loop in one function and the request execution in another but its still missbehaving as described previously. Is there anyone that could share their expertise please?

3 Answers 3

1
  1. The only way to do something after XMLHttpRequest has completed is to pass that "something" as a callback. For a chain of request, you'll have to do it recursively, so the first one will have to receive an ordered list of all the requests to execute after plus the finishing callback to do after the final one is completed. The code will probably get ugly, I'm not going to try.

  2. You can still send all your requests right away and render the data in the correct order as they come. Start with creating the basic skeleton structure of the output data

var outputStructure = '';

for (var i in unique_array) {
   var cardId = 'card-' + id;
   outputStructure += `<div class="card" id="card-${i}">`;
   outsideLoopRequest(unique_array[i], reqObject, i);
}

document.getElementById('accordion').innerHTML = outputStructure;

and on completion, put the data in the card with the right id.

    reqObject[i].onload = function() {
// create the output structure, then
       document.getElementById(`card-${i}`).innerHTML = output;
  1. Problems as such are a part of what is colloquially called "callback hell". It's simply so much easier to organize more complicated async operations (where some things can be done in parallel, others must wait until the previous one has completed) with promises. Do stuff after all requests have been completed? One line: Promise.all(requests).then(//....

Use fetch.

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

1 Comment

Thanks for the explanation. I think @Andy & @Takashi Harano showed your suggestion 1 & 3 in code, which I will study later. I implemented your 2, the only issue was the lack of </div> closing tag in outputStructure+= thus making cards within cards and output dispalying only 1 card.
1

Here is an implementation sample of you're trying to.
Each request will be executed one by one synchronously.

var index = 0;
var uniqueArray = ['a', 'b', 'c'];
var output = '';

function http(arrayValue) {
  var url = 'http://localhost:3000/resource/Projects/' + arrayValue;

  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (xhr.readyState == XMLHttpRequest.DONE) {
      onHttpDone(xhr);
    }
  };

  xhr.open('GET', url, true);
  xhr.send();
}

function onHttpDone(xhr) {
  if (xhr.status != 200) {
    console.log('HTTP ERROR: ' + xhr.status);
    return;
  }

  var client = JSON.parse(xhr.responseText);

  output += '<div class="card">' + client.someProperty + '</div>';
  document.getElementById('accordion').innerHTML = output;

  if (index < uniqueArray.length) {
    http(uniqueArray[index++]);
  }
}

http(uniqueArray[index++]);

Comments

1

Modern JS provides a number of new ways to work with async operations. Instead of calling the API and trying to return data with each iteration we can work with promises.

1) Make an array of promises to pass to Promise.all

This iterates over your array with map to produce a new array of promises produced with fetch (similar to XMLHTTPRequest but promise-based) and an endpoint based on the array element in that iteration. (I've used a template literal to create the endpoint string rather than string concatenation).

const promises = unique_array.map(el => {
  const endpoint = `http://localhost:3000/resource/Projects/${el}`;
  return fetch(endpoint);
});

2) Now we have an array of promises we simply need to give it to Promise.all which also returns a promise with the result of calling all those API endpoints. Using that data you can then loop over it to create your HTML.

Promise.all(promises).then(data => {
  // loop over the data to create the HTML
});

How about an example?

// We use a dummy fetch function to mimic a call to an
// API. Data is returned after two seconds. It is an object
// containing a number, and the square of that number
function dummyFetch(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ number: n, square: n * n });
    }, 2000);
  });
}

const arr = [1, 2, 3, 4];

// We produce the array of promises
const promises = arr.map(el => dummyFetch(el));

// When the promises have been resolved/rejected...
Promise.all(promises).then(data => {

  // ...we can iterate over the data and produce an
  // array of html
  const html = data.map(({ number, square }) => {
    return `
      <div>
        <span class="number">${number}</span>
        <span class="square">${square}</span>
      </div>
    `
  }).join('');

  // ...which we can then add to the page 
  document.body.insertAdjacentHTML('beforeend', html);
});
.number { color: red; }
.square { color: blue; }

Hope that helps.

Comments

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.