3

I have a dropdown of countries and have a jquery ajax call that populates a dropdown with the names of the countries. The problem is that I need to know when the call is complete before I can determine which country is selected. That is, any function I call after the function with the ajax call is called too soon (before the dropdown list is populated), so the following function will crash. I also do NOT want to use async=false (because that will cause more problems and is bad practice). What is the correct way to achieve this?

Here is a general purpose ajax function to retrieve data and populate a select dropdown (ddl) of a specific type with a parameter id:

   function RetrieveDD(ddl, ddtype, id) {
        var servicename;
        switch (ddtype) {
            case 'country':
                servicename = "LoadCountries";
                break;
            case 'state':
                servicename = "LoadStateProvinces";
                break;
            case 'city':
                servicename = "LoadCityLocalities";
                break;
            default:
                servicename = "";
        }
        if (servicename != "") {
             $.ajax({
                url: 'DDService.asmx/' + servicename,
                dataType: 'json',
                method: 'post',
                data: { ID: id },
                success: function (data) {
                    ddl.empty();
                    $.each(data, function (index) {
                        ddl.append($("<option" + (index == 0 ? " selected" : "") + "></option>").val(this['V']).html(this['T']));
                    });
                },
                error: function (err) {
                    console.log('Error (RetrieveDD): ' + JSON.stringify(err, null, 2));
                }
            });
        };
    };

I have a test button that executes this and want to get the value of a selected option (which will make a call to populate the states, get the value of the first selected state and call again to populate cities). Problem is pretty obvious: I can't say populate the next dropdown without knowing the selected value of the previous one (so I MUST have a way to WAIT for the dropdown to populate FIRST).

        $('#btnTest').click(function () {
           RetrieveDD($("#ddlCountries"), "country", 0);

            var cid = $("#ddlCountries").val();  //this is still undefined because ajax call above not completed 
            alert("cid = " + cid);    //normally another call would be made here to populate the states
        });

When clicking the button, you immediately see an alert box popup (with no value for cid) and the countries dropdown is not populated until after you click the [Okay] button from the alert dialog. It's in the wrong order, basically.

If I add async:false to the ajax routine, you will see the alert with the correct cid displayed, however, as people have posted that you should never use async:false. I wish to avoid this, but not sure how to do this correctly. (In case anyone is wondering, I'm removing all MSAjaxToolkit crap from an older dotnet webforms website and replacing with jQuery ajax - which is much faster, smaller, and easier to fix.)

here's the HTML snippet

    <input type="button" id="btnTest" value="Test" /><br />
    <div id="dd">
        <select id="ddlCountries"></select><br />
        <select id="ddlStates"></select><br />
        <select id="ddlCities"></select>
    </div>

************** in response to why I believe this not to be a duplicate ********* Needed to add the requirement for older IE support (which comes with Windows 7, 8, 8.1 and some server versions). IE doesn't support arrow functions or promises (or most ECMA methods after 2015) so a lot of recommended answers, while excellent, did not include code for such. I will try and figure this out and post here.

MC9000
  • 2,076
  • 7
  • 45
  • 80
  • You could move your `var cid = $("#ddlCountries").val();` into the success handler itself, or pass a callback into `RetrieveDD` that runs it (or use promises in a similar way). – Gavin Nov 15 '19 at 11:52

6 Answers6

2

JQuery.ajax() implements the Promise interface, so you can take the Promise object returned by the ajax call and pass it back to your calling function. That can use Promise.then() to call code after the ajax call has completed, I think this is neater than using callbacks.

   function RetrieveDD(ddl, ddtype, id) {
    var servicename;
    var promise;
    switch (ddtype) {
        case 'country':
            servicename = "LoadCountries";
            break;
        case 'state':
            servicename = "LoadStateProvinces";
            break;
        case 'city':
            servicename = "LoadCityLocalities";
            break;
        default:
            servicename = "";
    }
    if (servicename != "") {
         promise = $.ajax({
            url: 'DDService.asmx/' + servicename,
            dataType: 'json',
            method: 'post',
            data: { ID: id },
            success: function (data) {
                ddl.empty();
                $.each(data, function (index) {
                    ddl.append($("<option" + (index == 0 ? " selected" : "") + "></option>").val(this['V']).html(this['T']));
                    console.log('update list');
                });
            },
            error: function (err) {
                console.log('Error (RetrieveDD): ' + JSON.stringify(err, null, 2));
            }
        });
    };

    return promise;
};


$('#btnTest').click(function () {
    var promise = RetrieveDD($("#ddlCountries"), "country", 0);
    if (promise) {
        promise.then(function(value) {
            var cid = $("#ddlCountries").val();                  
            console.log('done');
        });
    }
});
freedomn-m
  • 27,664
  • 8
  • 35
  • 57
John M
  • 2,510
  • 6
  • 23
  • 31
2

Similar to John M's answer, but different usage.

Return the $.ajax promise and use .done()

if (servicename!="") 
    return $.ajax({...
// else return null

$('#btnTest').click(function () {
    var ajax = RetrieveDD($("#ddlCountries"), "country", 0);
    if (ajax) {  
        ajax.done(function(data) {  
            console.log('done');
        });
    }
});
freedomn-m
  • 27,664
  • 8
  • 35
  • 57
  • IE doesn't have support for this, unfortunately. Nor fat arrows. (at least not the versions that come with Win 8.1 and earlier) . I wish IE would just die. – MC9000 Nov 16 '19 at 01:57
  • There's no 'fat arrows' here. Which part do you think it doesn't have support for? `return` ? – freedomn-m Nov 16 '19 at 09:39
1

Use a callback in your function:

function RetrieveDD(ddl, ddtype, id, callback = false) {
    // ...
    if (servicename != "") {
         $.ajax({
            url: 'DDService.asmx/' + servicename,
            dataType: 'json',
            method: 'post',
            data: { ID: id },
            success: function (data) {
                // ...
                if(typeof callback === 'function'){
                    callback(data);
                }
            },
            error: function (err) {
                // ...
                if(typeof callback === 'function'){
                    callback(err);
                }
            }
        });
    };
};

RetrieveDD($("#ddlCountries"), "country", 0, (data_or_error)=>{
    var cid = $("#ddlCountries").val();
});
Gavin
  • 2,214
  • 2
  • 18
  • 26
  • This is exactly what I was looking for! Honestly wasn't sure how to do this with javascript, but this makes sense to me being a server-side dev mostly. – MC9000 Nov 15 '19 at 13:48
  • 1
    If you want to be really hip and modern you could use promises too. Probably overkill at this stage tho. – Gavin Nov 15 '19 at 13:53
  • Just need to figure out how to convert it from a fat arrow to the older syntax since half of IE users are still using Windows 8.1 and below (which doesn't support version 6). – MC9000 Nov 16 '19 at 01:54
  • In that example the classic `function(data_or_error){}` will work fine in place of `(data_or_error)=>{}`. – Gavin Nov 16 '19 at 12:30
  • I got it figured out - see my answer. thanks. – MC9000 Nov 16 '19 at 18:01
1

Ajax has its own success,error,complete callback

$.ajax({
type:"POST",
url:"loadData",
data:{id:id},
success:function(data){
console.log(data);
},
error:function(err){
console.log(err);
},
complete:function(){
console.log("Ajax call completed !!!!");
}
})


Pratham
  • 259
  • 1
  • 8
  • 1
    Hi, this would be a useful answer, but OP is already using these as demonstrated in their code. They need to know how to access these from a calling function. – freedomn-m Nov 15 '19 at 12:20
1

i think you can use this syntax in ajax call

$.ajax({
            type: 'post',
            url: 'your url',
            data: new FormData(this),
            contentType: false,
            data_type: 'json',
            cache: false,
            processData: false,
            beforesend: function () {
                //your code
            },
            success: function (data) {
                // your code
            }, complete: function () {
                // your code
            }
        });
Hassan Fayyaz
  • 685
  • 1
  • 9
  • 15
  • OP is already using this syntax as shown in their code snippet. They need to know how to access these from a calling function. – freedomn-m Nov 15 '19 at 12:29
0

Here is what I came up with that works (with a little help from a transpiler es6console.com). I left the original versions in commented areas for reference. The extra bits are for toggling Other fields (definitely needs more cleaning up/streamlining) appropriately ("Other" is the last option in all the dropdowns - though I don't think a new country will sprout up any time soon, unless it's on Mars. lol!)

$(document).ready(function () {


    $('#btnTest').click(function () {
        //RetrieveDD($("#ddlCountries"), "country", 0, (data_or_error) => {
        //    LoadStates();
        //});
        // IE compatible
        RetrieveDD($("#ddlCountries"), "country", 0, function (data_or_error) {
            LoadStates();
        });
    });

    function LoadStates() {
        Visible("#txtOtherCountry", 0);
        var cid = $('#ddlCountries').val();
        if (cid > -1) {
            if (cid == 0) {
                //other - turn on Other Country field
                Visible("#txtOtherCountry", 1);
                // set both state & city dropdowns to Other and make their Other fields display
                // clear state dropdown & add "Other" with a value of 0 to it & open
                SetOtherState();
                SetOtherCity();
            } else {
                //populate states for selected country
                //RetrieveDD($("#ddlStates"), "state", cid, (data_or_error) => {
                //    LoadCities();
                //});
                // IE compatible
                RetrieveDD($("#ddlStates"), "state", cid, function (data_or_error) {
                    LoadCities();
                });
            }
        } else {
            // make selection (future)
        }
    }

    function SetOtherState() {
        var ddlState = $("#ddlStates");
        ddlState.empty();
        ddlState.append($("<option selected></option>").val("0").html("Other"));
        Visible("#txtOtherState", 1);
    }

    function SetOtherCity() {
        var ddlCity = $("#ddlCities");
        ddlCity.empty();
        ddlCity.append($("<option selected></option>").val("0").html("Other"));
        Visible("#txtOtherCity", 1);
    }

    function Visible(elem, mode) {
        $(elem).val('');
        $(elem).css('visibility', (mode == 1 ? "visible" : "hidden"));
    }

    function LoadCities() {
        Visible("#txtOtherState", 0);
        Visible("#txtOtherCity", 0);
        var sid = $('#ddlStates').val();
        if (sid > -1) {
            if (sid == 0) {
                //other - turn on Other State field
                Visible("#txtOtherState", 1);
                // clear city dropdown & add "Other" with a value of 0 to it & open
                SetOtherCity();
            } else {
                RetrieveDD($("#ddlCities"), "city", sid);
            }
        } else {
            // make selection (future)
        }
    }


    $("#ddlCountries").change(function () {
        LoadStates();
    });

    $("#ddlStates").change(function () {
        LoadCities();
    });


    $("#ddlCities").change(function () {
        if ($("#ddlCities").val() == 0) {
            Visible("#txtOtherCity", 1);
        } else {
            Visible("#txtOtherCity", 0);
        };
    });


    //function RetrieveDD(ddl, ddtype, id, callback = false) {
    //    var servicename;
    //    switch (ddtype) {
    //        case 'country':
    //            servicename = "LoadCountries";
    //            break;
    //        case 'state':
    //            servicename = "LoadStateProvinces";
    //            break;
    //        case 'city':
    //            servicename = "LoadCityLocalities";
    //            break;
    //        default:
    //            servicename = "";
    //    }
    //    if (servicename != "") {
    //        var oldEvent = ddl.attr("onchange");
    //        ddl.attr("onchange", ""); //remove change event
    //        $.ajax({
    //            url: 'DDService.asmx/' + servicename,
    //            dataType: 'json',
    //            method: 'post',
    //            data: { ID: id },
    //            success: function (data) {
    //                ddl.empty();
    //                $.each(data, function (index) {
    //                    ddl.append($("<option" + (index == 0 ? " selected" : "") + "></option>").val(this['V']).html(this['T']));
    //                });
    //                if (typeof callback === 'function') {
    //                    callback(data);
    //                }
    //            },
    //            error: function (err) {
    //                console.log('Error (RetrieveDD): ' + JSON.stringify(err, null, 2));
    //                if (typeof callback === 'function') {
    //                    callback(err);
    //                }
    //            }
    //        });
    //        ddl.attr("onchange", oldEvent); //add change event back
    //    };
    //};

    //IE compatible
    function RetrieveDD(ddl, ddtype, id) {
        var callback = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];

        var servicename;
        switch (ddtype) {
            case 'country':
                servicename = "LoadCountries";
                break;
            case 'state':
                servicename = "LoadStateProvinces";
                break;
            case 'city':
                servicename = "LoadCityLocalities";
                break;
            default:
                servicename = "";
        }
        if (servicename != "") {
            var oldEvent = ddl.attr("onchange");
            ddl.attr("onchange", "");
            $.ajax({
                url: 'DDService.asmx/' + servicename,
                dataType: 'json',
                method: 'post',
                data: { ID: id },
                success: function success(data) {
                    ddl.empty();
                    $.each(data, function (index) {
                        ddl.append($("<option" + (index == 0 ? " selected" : "") + "></option>").val(this['V']).html(this['T']));
                    });
                    if (typeof callback === 'function') {
                        callback(data);
                    }
                },
                error: function error(err) {
                    console.log('Error (RetrieveDD): ' + JSON.stringify(err, null, 2));
                    if (typeof callback === 'function') {
                        callback(err);
                    }
                }
            });
            ddl.attr("onchange", oldEvent);
        };
    };
});
MC9000
  • 2,076
  • 7
  • 45
  • 80