0

I have an ajax code like that:

$.ajax({
    type: "post",
    async: false,
    url: "/FindAVet/Search",
    data: '{"vetname":"' + $("#VetName").val() + '","lat":"' + objlatitude + '","lng":"' + objlongitude + '","radius":"' + $("#hdnRadius").val() + '","searchAll":"' + searchAll + '"}',
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (result) {
        if (result.IsValid == true) {
            $("#divMapContainer").show();
            $("#placeholderServiceagent").html('');
            $("#placeholderServiceagent").show();
            $("#placeholderServiceagent").html(result.Datavalue);
            $("#noserviceagentstatus").hide();

            var arrLoc = result.Locations.split(";");
            var arrInf = result.InfoWindowContents.split(";");

            var source, destination, distance;
            source = $("#SuburbOrPostcode").val();

            var service = new google.maps.DistanceMatrixService();

            var arr2dLoc = [];
            var arr2dInf = [];
            for (var i = 0; i < arrLoc.length; i++) {
                arr2dLoc[i] = arrLoc[i].split(",");
            }
            var j = 1;
            for (var i = 0; i < arrInf.length; i++) {

                arr2dInf[i] = arrInf[i].split(",");
                var strarr2dInf = arr2dInf[i].toString();
                var dom_des = $($.parseHTML(strarr2dInf));
                destination = dom_des.find('.des').text();
                service.getDistanceMatrix({
                    origins: [source],
                    destinations: [destination],
                    travelMode: google.maps.TravelMode.DRIVING,
                    unitSystem: google.maps.UnitSystem.METRIC,
                    avoidHighways: false,
                    avoidTolls: false
                }, function (response, status) {
                    if (status == google.maps.DistanceMatrixStatus.OK && response.rows[0].elements[0].status != "ZERO_RESULTS") {
                        distance = response.rows[0].elements[0].distance.text;
                        var spDistance = $("#distance" + j);
                        spDistance.prepend(distance);

                        var li = $("#" + j);
                        var distancewithoutkmtext = distance.replace(' km', '');
                        li.attr("id", distancewithoutkmtext);

                        j++;
                    } else {
                            alert("Unable to find the distance via road.");
                        }
                    });
            }



            init_map('map_canvas', 18, arr2dLoc, arr2dInf);
        }
        else {
            $("#divMapContainer").hide();
            $("#placeholderServiceagent").hide();
            $("#noserviceagentstatus").show();
        }
        $("#lblServiceAgentStatus").html(result.Message);

    },
    complete: function (data) {

        var elems = $('.storeList').children('li');
        elems.each(function (idx, li) {
            alert($(this).attr("id"));
        });
    }

I have already changed the id of li tag like that:

 var li = $("#" + j);
 var distancewithoutkmtext = distance.replace(' km', '');
 li.attr("id", distancewithoutkmtext);

And the output html is rendered like that: enter image description here which is correct (with the new ids is updated)

But in complete function, when i try to test by calling alert function to show ids of <li> tags, the old value of id is displayed yet like that: enter image description here which is wrong (1,2,3,4... are original values of id of li tags)

Can you help me guys? Any helps will be much appreciated. Thank you so much.

Miron
  • 1,007
  • 2
  • 17
  • 31
Huy Ta
  • 115
  • 10
  • have you tried with `$(li).attr("id")` – Carsten Løvbo Andersen Nov 23 '17 at 08:02
  • where should i put $(li).attr("id") to ? @ Carsten Løvbo Andersen Thank you – Huy Ta Nov 23 '17 at 08:03
  • at your alert `alert($(this).attr("id"));` – Carsten Løvbo Andersen Nov 23 '17 at 08:05
  • Nothing to happens, the same what i got – Huy Ta Nov 23 '17 at 08:54
  • I didn't know `complete` option. Usually I put the code which you put into `complete`, at the end of `success` handler. And what do you expect `this` could be inside the `complete` handler? Did you try to print `this` on console? – Miron Nov 23 '17 at 09:15
  • In my opinion, the problem is that service.getDistanceMatrix callback function gets called AFTER the complete callback. You got yourself into a callback hell, of course you can use jQuery and somehow make it work, but for god's sake, throw it away and use fetch for requests instead, promises for completion. – netchkin Nov 23 '17 at 09:17
  • @netchkin, I agree with you. I suffered from that kind of problems before, but I don't know how to deal with asynchronous functions yet. Could you guide us how to overcome this asynchronous functions? Then it could be answer of this [question](https://stackoverflow.com/questions/46326212/how-to-return-indexeddb-query-result-out-of-event-handler), too. – Miron Nov 23 '17 at 10:06
  • Thank you @ntchkin, can you guide me how to ? – Huy Ta Nov 23 '17 at 10:36
  • @Miron of course, first, sorry for my rant. I'll post an answer later today as I need to wrap up at my job first – netchkin Nov 23 '17 at 13:42

2 Answers2

0

You're changing the IDs in the asynchronous callback function of service.getDistanceMatrix(). But you're alerting the IDs in the callback function of the original $.ajax, so the second callback function hasn't run yet.

Move the loop with the alerts to that second callback function and you'll see the correct results.

$.ajax({
  type: "post",
  async: false,
  url: "/FindAVet/Search",
  data: '{"vetname":"' + $("#VetName").val() + '","lat":"' + objlatitude + '","lng":"' + objlongitude + '","radius":"' + $("#hdnRadius").val() + '","searchAll":"' + searchAll + '"}',
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  success: function(result) {
    if (result.IsValid == true) {
      $("#divMapContainer").show();
      $("#placeholderServiceagent").html('');
      $("#placeholderServiceagent").show();
      $("#placeholderServiceagent").html(result.Datavalue);
      $("#noserviceagentstatus").hide();

      var arrLoc = result.Locations.split(";");
      var arrInf = result.InfoWindowContents.split(";");

      var source, destination, distance;
      source = $("#SuburbOrPostcode").val();

      var service = new google.maps.DistanceMatrixService();

      var arr2dLoc = [];
      var arr2dInf = [];
      for (var i = 0; i < arrLoc.length; i++) {
        arr2dLoc[i] = arrLoc[i].split(",");
      }
      var j = 1;
      for (var i = 0; i < arrInf.length; i++) {

        arr2dInf[i] = arrInf[i].split(",");
        var strarr2dInf = arr2dInf[i].toString();
        var dom_des = $($.parseHTML(strarr2dInf));
        destination = dom_des.find('.des').text();
        service.getDistanceMatrix({
          origins: [source],
          destinations: [destination],
          travelMode: google.maps.TravelMode.DRIVING,
          unitSystem: google.maps.UnitSystem.METRIC,
          avoidHighways: false,
          avoidTolls: false
        }, function(response, status) {
          if (status == google.maps.DistanceMatrixStatus.OK && response.rows[0].elements[0].status != "ZERO_RESULTS") {
            distance = response.rows[0].elements[0].distance.text;
            var spDistance = $("#distance" + j);
            spDistance.prepend(distance);

            var li = $("#" + j);
            var distancewithoutkmtext = distance.replace(' km', '');
            li.attr("id", distancewithoutkmtext);

            j++;
          } else {
            alert("Unable to find the distance via road.");
          }
          var elems = $('.storeList').children('li');
          elems.each(function(idx, li) {
            alert($(this).attr("id"));
          });
        });
      }
      init_map('map_canvas', 18, arr2dLoc, arr2dInf);
    } else {
      $("#divMapContainer").hide();
      $("#placeholderServiceagent").hide();
      $("#noserviceagentstatus").show();
    }
    $("#lblServiceAgentStatus").html(result.Message);
  }
});
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Thank you @Barmar. #Barmar Can you put the code with updated here here so i can easily understand. – Huy Ta Nov 23 '17 at 10:32
  • I've updated the answer. This will produce lots of alerts, because each time through the `for` loop it will alert all the IDs. – Barmar Nov 23 '17 at 10:49
0

I got tagled in your code far to much now, so I'll try to explain things in an example

Say that we can rewrite your code so that it looks like this:

$.ajax({
  type: "post",
  url: "/FindAVet/Search",
  // etc
  success: function successHandler(result) {
    // ...
  },
  complete: function completeHandler() {
    // ...
  }
})

now, let's take a look inside the successHadler

function successHandler(result) {
  // Do stuff with html
  // parse result
  // for each location;info pair:
  //   call service.getDistanceMatrix
  //   parse distanceMatrix
  //   modify DOM accordingly
}

in completeHandler, presumably, you want to work further with the modified DOM, but you cannot. And that's because call to service.getDistanceMatrix is asynchronous. In reality, $.ajax

  1. issues a request
  2. waits. <--- this is hugely important. It's not a plain wait. you essentially yield execution control, allowing other code to run Event loop takes charge here, for more details, see https://developer.mozilla.org/cs/docs/Web/JavaScript/EventLoop
  3. after response arrives, calls success (while we're waiting, this is placed on the event loop queue)
  4. after success finishes, calls complete (again, queued right after success)

That wait thing that happens, that happens with every asynchronous call. And since service.getDistanceMatrix is asynchronous, after it calls to google, it WAITS. so, ajax does 1, 2, 3. During 3. service.getDistanceMatrix gets called.

service.getDistanceMatrix inside has a similar mechanism as $.ajax. So again, it waits. Now, if something waits, event loop looks into the queue for events that it could work on in the meantime. And, it sees now that there is step 4. from the previous $.ajax, so it happily goes to work with it. After a few ms, service.getDistanceMatrix also gets data and fires off your callback, that contains:

//   parse distanceMatrix
//   modify DOM accordingly

But that's far too late, since completeHandler already finished

So, how to get out of this?

Simplest solution would be to put the code from the completeHandler inside the successHandler. But that's not all.

$.ajax({
  type: "post",
  url: "/FindAVet/Search",
  // etc
  success: function successHandler(result) {
    // Do stuff with html
    // parse result
    // for each location;info pair:
    //   call service.getDistanceMatrix
    //   parse distanceMatrix
    //   modify DOM accordingly

    completeHandler();
  }
})

complete handler now still does not wait for the asynchronous calls to getDistanceMatrix to finish. We can wrap the service.getDistanceMatrix into promise to make it asynchronous:

const service = new google.maps.DistanceMatrixService()
function getDistanceMatrixAsync(parameters) {
  return new Promise(function(resolve, reject) {
    service.getDistanceMatrix(parameters, function(response, status) {
      if (status == google.maps.DistanceMatrixStatus.OK && response.rows[0].elements[0].status != "ZERO_RESULTS") {
        resolve(response)
      }
      else {
        reject('Unable to find the distance via road.')
      }
    })
  })
}

// usage:

// outside the loop - this is needed because of the asynchronous code inside the for loop.
let k = 1

// inside the loop
getDistanceMatrixAsync({
  origins: [source],
  destinations: [destination],
  travelMode: google.maps.TravelMode.DRIVING,
  unitSystem: google.maps.UnitSystem.METRIC,
  avoidHighways: false,
  avoidTolls: false
})
.then(function (response) {
  let j = k;
  distance = response.rows[0].elements[0].distance.text;
  var spDistance = $("#distance" + j);
  spDistance.prepend(distance);

  var li = $("#" + j);
  var distancewithoutkmtext = distance.replace(' km', '');
  li.attr("id", distancewithoutkmtext)
})
.catch(function (error) {
  alert(error)
})

k++;

still, we are not done. The result of this call is still Promise. Still an asynchronous call, completeHandler does not wait on this. But we're almost there. We need to produce a synchronization mechanism called Barrier. It's nothing fancy, just waits on multiple asynchronous tasks (promises) to finish, and once all are done, it executes code you want. But for that, we need to create an array of these Promises!

  // create array for promises just before the for loop
  var promises = []

  // add the promise to the array
  var promise = getDistanceMatrixAsync(/* data... */).then(/* stuff with the distance */).catch(/*optional error handling*/)
  promises.push(promise)

  // finally, outside and after the for loop, create a barrier!
  Promise.all(promises).then(function() { completeHandler() })

So this is the shortest path to success! It's incredibly far from clean and maintainable, but it should be working. If you're interested in improving maintainability, ping me and we can go thru it as well.

recommended reading:


EDIT:

fixed error with IDs in then getDistanceMatrixAsync. Note that it changes IDs numbering logic for li tags due to the fact that the Promise resolutions can arrive out-of-order. I'd suggest using surrogate IDs, i.e. ID that's not tied to any logic inside your application (currently you use position in a list, which leads to coupling)

netchkin
  • 1,387
  • 1
  • 12
  • 21